mirror of https://github.com/prometheus/prometheus
Add DigitalOcean service discovery (#7407)
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>pull/7421/head^2
parent
7b4f81b397
commit
c61141ce51
@ -0,0 +1,193 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/go-kit/kit/log"
|
||||
config_util "github.com/prometheus/common/config"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/common/version"
|
||||
|
||||
"github.com/prometheus/prometheus/discovery/refresh"
|
||||
"github.com/prometheus/prometheus/discovery/targetgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
doLabel = model.MetaLabelPrefix + "digitalocean_"
|
||||
doLabelID = doLabel + "droplet_id"
|
||||
doLabelName = doLabel + "droplet_name"
|
||||
doLabelImage = doLabel + "image"
|
||||
doLabelPrivateIPv4 = doLabel + "private_ipv4"
|
||||
doLabelPublicIPv4 = doLabel + "public_ipv4"
|
||||
doLabelPublicIPv6 = doLabel + "public_ipv6"
|
||||
doLabelRegion = doLabel + "region"
|
||||
doLabelSize = doLabel + "size"
|
||||
doLabelStatus = doLabel + "status"
|
||||
doLabelFeatures = doLabel + "features"
|
||||
doLabelTags = doLabel + "tags"
|
||||
separator = ","
|
||||
)
|
||||
|
||||
// DefaultSDConfig is the default DigitalOcean SD configuration.
|
||||
var DefaultSDConfig = SDConfig{
|
||||
Port: 80,
|
||||
RefreshInterval: model.Duration(60 * time.Second),
|
||||
}
|
||||
|
||||
// SDConfig is the configuration for DigitalOcean based service discovery.
|
||||
type SDConfig struct {
|
||||
HTTPClientConfig config_util.HTTPClientConfig `yaml:",inline"`
|
||||
|
||||
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
*c = DefaultSDConfig
|
||||
type plain SDConfig
|
||||
err := unmarshal((*plain)(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Discovery periodically performs DigitalOcean requests. It implements
|
||||
// the Discoverer interface.
|
||||
type Discovery struct {
|
||||
*refresh.Discovery
|
||||
client *godo.Client
|
||||
port int
|
||||
}
|
||||
|
||||
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
|
||||
func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
|
||||
d := &Discovery{
|
||||
port: conf.Port,
|
||||
}
|
||||
|
||||
rt, err := config_util.NewRoundTripperFromConfig(conf.HTTPClientConfig, "digitalocean_sd", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.client, err = godo.New(
|
||||
&http.Client{
|
||||
Transport: rt,
|
||||
Timeout: 5 * time.Duration(conf.RefreshInterval),
|
||||
},
|
||||
godo.SetUserAgent(fmt.Sprintf("Prometheus/%s", version.Version)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error setting up digital ocean agent: %w", err)
|
||||
}
|
||||
|
||||
d.Discovery = refresh.NewDiscovery(
|
||||
logger,
|
||||
"digitalocean",
|
||||
time.Duration(conf.RefreshInterval),
|
||||
d.refresh,
|
||||
)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
|
||||
tg := &targetgroup.Group{
|
||||
Source: "DigitalOcean",
|
||||
}
|
||||
|
||||
droplets, err := d.listDroplets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, droplet := range droplets {
|
||||
if droplet.Networks == nil || len(droplet.Networks.V4) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
privateIPv4, err := droplet.PrivateIPv4()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while reading private IPv4 of droplet %d: %w", droplet.ID, err)
|
||||
}
|
||||
publicIPv4, err := droplet.PublicIPv4()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while reading public IPv4 of droplet %d: %w", droplet.ID, err)
|
||||
}
|
||||
publicIPv6, err := droplet.PublicIPv6()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while reading public IPv6 of droplet %d: %w", droplet.ID, err)
|
||||
}
|
||||
|
||||
labels := model.LabelSet{
|
||||
doLabelID: model.LabelValue(fmt.Sprintf("%d", droplet.ID)),
|
||||
doLabelName: model.LabelValue(droplet.Name),
|
||||
doLabelImage: model.LabelValue(droplet.Image.Slug),
|
||||
doLabelPrivateIPv4: model.LabelValue(privateIPv4),
|
||||
doLabelPublicIPv4: model.LabelValue(publicIPv4),
|
||||
doLabelPublicIPv6: model.LabelValue(publicIPv6),
|
||||
doLabelRegion: model.LabelValue(droplet.Region.Slug),
|
||||
doLabelSize: model.LabelValue(droplet.SizeSlug),
|
||||
doLabelStatus: model.LabelValue(droplet.Status),
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(publicIPv4, strconv.FormatUint(uint64(d.port), 10))
|
||||
labels[model.AddressLabel] = model.LabelValue(addr)
|
||||
|
||||
if len(droplet.Features) > 0 {
|
||||
// We surround the separated list with the separator as well. This way regular expressions
|
||||
// in relabeling rules don't have to consider feature positions.
|
||||
features := separator + strings.Join(droplet.Features, separator) + separator
|
||||
labels[doLabelFeatures] = model.LabelValue(features)
|
||||
}
|
||||
|
||||
if len(droplet.Tags) > 0 {
|
||||
// We surround the separated list with the separator as well. This way regular expressions
|
||||
// in relabeling rules don't have to consider tag positions.
|
||||
tags := separator + strings.Join(droplet.Tags, separator) + separator
|
||||
labels[doLabelTags] = model.LabelValue(tags)
|
||||
}
|
||||
|
||||
tg.Targets = append(tg.Targets, labels)
|
||||
}
|
||||
return []*targetgroup.Group{tg}, nil
|
||||
}
|
||||
|
||||
func (d *Discovery) listDroplets() ([]godo.Droplet, error) {
|
||||
var (
|
||||
droplets []godo.Droplet
|
||||
opts = &godo.ListOptions{Page: 1}
|
||||
)
|
||||
for {
|
||||
paginatedDroplets, resp, err := d.client.Droplets.List(context.Background(), opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while listing droplets page %d: %w", opts.Page, err)
|
||||
}
|
||||
droplets = append(droplets, paginatedDroplets...)
|
||||
if resp.Links == nil || resp.Links.IsLastPage() {
|
||||
break
|
||||
}
|
||||
opts.Page++
|
||||
}
|
||||
return droplets, nil
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/util/testutil"
|
||||
)
|
||||
|
||||
type DigitalOceanSDTestSuite struct {
|
||||
Mock *SDMock
|
||||
}
|
||||
|
||||
func (s *DigitalOceanSDTestSuite) TearDownSuite() {
|
||||
s.Mock.ShutdownServer()
|
||||
}
|
||||
|
||||
func (s *DigitalOceanSDTestSuite) SetupTest(t *testing.T) {
|
||||
s.Mock = NewSDMock(t)
|
||||
s.Mock.Setup()
|
||||
|
||||
s.Mock.HandleDropletsList()
|
||||
}
|
||||
|
||||
func TestDigitalOceanSDRefresh(t *testing.T) {
|
||||
sdmock := &DigitalOceanSDTestSuite{}
|
||||
sdmock.SetupTest(t)
|
||||
t.Cleanup(sdmock.TearDownSuite)
|
||||
|
||||
cfg := DefaultSDConfig
|
||||
cfg.HTTPClientConfig.BearerToken = tokenID
|
||||
d, err := NewDiscovery(&cfg, log.NewNopLogger())
|
||||
testutil.Ok(t, err)
|
||||
endpoint, err := url.Parse(sdmock.Mock.Endpoint())
|
||||
testutil.Ok(t, err)
|
||||
d.client.BaseURL = endpoint
|
||||
|
||||
ctx := context.Background()
|
||||
tgs, err := d.refresh(ctx)
|
||||
testutil.Ok(t, err)
|
||||
|
||||
testutil.Equals(t, 1, len(tgs))
|
||||
|
||||
tg := tgs[0]
|
||||
testutil.Assert(t, tg != nil, "tg should not be nil")
|
||||
testutil.Assert(t, tg.Targets != nil, "tg.targets should not be nil")
|
||||
testutil.Equals(t, 4, len(tg.Targets))
|
||||
|
||||
for i, lbls := range []model.LabelSet{
|
||||
{
|
||||
"__address__": model.LabelValue("104.236.32.182:80"),
|
||||
"__meta_digitalocean_droplet_id": model.LabelValue("3164444"),
|
||||
"__meta_digitalocean_droplet_name": model.LabelValue("example.com"),
|
||||
"__meta_digitalocean_image": model.LabelValue("ubuntu-16-04-x64"),
|
||||
"__meta_digitalocean_private_ipv4": model.LabelValue(""),
|
||||
"__meta_digitalocean_public_ipv4": model.LabelValue("104.236.32.182"),
|
||||
"__meta_digitalocean_public_ipv6": model.LabelValue("2604:A880:0800:0010:0000:0000:02DD:4001"),
|
||||
"__meta_digitalocean_region": model.LabelValue("nyc3"),
|
||||
"__meta_digitalocean_size": model.LabelValue("s-1vcpu-1gb"),
|
||||
"__meta_digitalocean_status": model.LabelValue("active"),
|
||||
"__meta_digitalocean_features": model.LabelValue(",backups,ipv6,virtio,"),
|
||||
},
|
||||
{
|
||||
"__address__": model.LabelValue("104.131.186.241:80"),
|
||||
"__meta_digitalocean_droplet_id": model.LabelValue("3164494"),
|
||||
"__meta_digitalocean_droplet_name": model.LabelValue("prometheus"),
|
||||
"__meta_digitalocean_image": model.LabelValue("ubuntu-16-04-x64"),
|
||||
"__meta_digitalocean_private_ipv4": model.LabelValue(""),
|
||||
"__meta_digitalocean_public_ipv4": model.LabelValue("104.131.186.241"),
|
||||
"__meta_digitalocean_public_ipv6": model.LabelValue(""),
|
||||
"__meta_digitalocean_region": model.LabelValue("nyc3"),
|
||||
"__meta_digitalocean_size": model.LabelValue("s-1vcpu-1gb"),
|
||||
"__meta_digitalocean_status": model.LabelValue("active"),
|
||||
"__meta_digitalocean_tags": model.LabelValue(",monitor,"),
|
||||
"__meta_digitalocean_features": model.LabelValue(",virtio,"),
|
||||
},
|
||||
{
|
||||
"__address__": model.LabelValue("167.172.111.118:80"),
|
||||
"__meta_digitalocean_droplet_id": model.LabelValue("175072239"),
|
||||
"__meta_digitalocean_droplet_name": model.LabelValue("prometheus-demo-old"),
|
||||
"__meta_digitalocean_image": model.LabelValue("ubuntu-18-04-x64"),
|
||||
"__meta_digitalocean_private_ipv4": model.LabelValue("10.135.64.211"),
|
||||
"__meta_digitalocean_public_ipv4": model.LabelValue("167.172.111.118"),
|
||||
"__meta_digitalocean_public_ipv6": model.LabelValue(""),
|
||||
"__meta_digitalocean_region": model.LabelValue("fra1"),
|
||||
"__meta_digitalocean_size": model.LabelValue("s-1vcpu-1gb"),
|
||||
"__meta_digitalocean_status": model.LabelValue("off"),
|
||||
"__meta_digitalocean_features": model.LabelValue(",ipv6,private_networking,"),
|
||||
},
|
||||
{
|
||||
"__address__": model.LabelValue("138.65.56.69:80"),
|
||||
"__meta_digitalocean_droplet_id": model.LabelValue("176011507"),
|
||||
"__meta_digitalocean_droplet_name": model.LabelValue("prometheus-demo"),
|
||||
"__meta_digitalocean_image": model.LabelValue("ubuntu-18-04-x64"),
|
||||
"__meta_digitalocean_private_ipv4": model.LabelValue("10.135.64.212"),
|
||||
"__meta_digitalocean_public_ipv4": model.LabelValue("138.65.56.69"),
|
||||
"__meta_digitalocean_public_ipv6": model.LabelValue("2a03:b0c0:3:f0::cf2:4"),
|
||||
"__meta_digitalocean_region": model.LabelValue("fra1"),
|
||||
"__meta_digitalocean_size": model.LabelValue("s-1vcpu-1gb"),
|
||||
"__meta_digitalocean_status": model.LabelValue("active"),
|
||||
"__meta_digitalocean_features": model.LabelValue(",ipv6,private_networking,"),
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
|
||||
testutil.Equals(t, lbls, tg.Targets[i])
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,644 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// SDMock is the interface for the DigitalOcean mock
|
||||
type SDMock struct {
|
||||
t *testing.T
|
||||
Server *httptest.Server
|
||||
Mux *http.ServeMux
|
||||
}
|
||||
|
||||
// NewSDMock returns a new SDMock.
|
||||
func NewSDMock(t *testing.T) *SDMock {
|
||||
return &SDMock{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint returns the URI to the mock server
|
||||
func (m *SDMock) Endpoint() string {
|
||||
return m.Server.URL + "/"
|
||||
}
|
||||
|
||||
// Setup creates the mock server
|
||||
func (m *SDMock) Setup() {
|
||||
m.Mux = http.NewServeMux()
|
||||
m.Server = httptest.NewServer(m.Mux)
|
||||
}
|
||||
|
||||
// ShutdownServer creates the mock server
|
||||
func (m *SDMock) ShutdownServer() {
|
||||
m.Server.Close()
|
||||
}
|
||||
|
||||
const tokenID = "3c9a75a2-24fd-4508-b4f2-11f18aa97411"
|
||||
|
||||
// HandleDropletsList mocks droplet list.
|
||||
func (m *SDMock) HandleDropletsList() {
|
||||
m.Mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", tokenID) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("content-type", "application/json; charset=utf-8")
|
||||
w.Header().Add("ratelimit-limit", "1200")
|
||||
w.Header().Add("ratelimit-remaining", "965")
|
||||
w.Header().Add("ratelimit-reset", "1415984218")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
|
||||
page := 1
|
||||
if pageQuery, ok := r.URL.Query()["page"]; ok {
|
||||
var err error
|
||||
page, err = strconv.Atoi(pageQuery[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, []string{`
|
||||
{
|
||||
"droplets": [
|
||||
{
|
||||
"id": 3164444,
|
||||
"name": "example.com",
|
||||
"memory": 1024,
|
||||
"vcpus": 1,
|
||||
"disk": 25,
|
||||
"locked": false,
|
||||
"status": "active",
|
||||
"kernel": {
|
||||
"id": 2233,
|
||||
"name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic",
|
||||
"version": "3.13.0-37-generic"
|
||||
},
|
||||
"created_at": "2014-11-14T16:29:21Z",
|
||||
"features": [
|
||||
"backups",
|
||||
"ipv6",
|
||||
"virtio"
|
||||
],
|
||||
"backup_ids": [
|
||||
7938002
|
||||
],
|
||||
"snapshot_ids": [
|
||||
|
||||
],
|
||||
"image": {
|
||||
"id": 6918990,
|
||||
"name": "14.04 x64",
|
||||
"distribution": "Ubuntu",
|
||||
"slug": "ubuntu-16-04-x64",
|
||||
"public": true,
|
||||
"regions": [
|
||||
"nyc1",
|
||||
"ams1",
|
||||
"sfo1",
|
||||
"nyc2",
|
||||
"ams2",
|
||||
"sgp1",
|
||||
"lon1",
|
||||
"nyc3",
|
||||
"ams3",
|
||||
"nyc3"
|
||||
],
|
||||
"created_at": "2014-10-17T20:24:33Z",
|
||||
"type": "snapshot",
|
||||
"min_disk_size": 20,
|
||||
"size_gigabytes": 2.34
|
||||
},
|
||||
"volume_ids": [
|
||||
|
||||
],
|
||||
"size": {
|
||||
},
|
||||
"size_slug": "s-1vcpu-1gb",
|
||||
"networks": {
|
||||
"v4": [
|
||||
{
|
||||
"ip_address": "104.236.32.182",
|
||||
"netmask": "255.255.192.0",
|
||||
"gateway": "104.236.0.1",
|
||||
"type": "public"
|
||||
}
|
||||
],
|
||||
"v6": [
|
||||
{
|
||||
"ip_address": "2604:A880:0800:0010:0000:0000:02DD:4001",
|
||||
"netmask": 64,
|
||||
"gateway": "2604:A880:0800:0010:0000:0000:0000:0001",
|
||||
"type": "public"
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"name": "New York 3",
|
||||
"slug": "nyc3",
|
||||
"sizes": [
|
||||
|
||||
],
|
||||
"features": [
|
||||
"virtio",
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata"
|
||||
],
|
||||
"available": null
|
||||
},
|
||||
"tags": [
|
||||
|
||||
],
|
||||
"vpc_uuid": "f9b0769c-e118-42fb-a0c4-fed15ef69662"
|
||||
},
|
||||
{
|
||||
"id": 3164494,
|
||||
"name": "prometheus",
|
||||
"memory": 1024,
|
||||
"vcpus": 1,
|
||||
"disk": 25,
|
||||
"locked": false,
|
||||
"status": "active",
|
||||
"kernel": {
|
||||
"id": 2233,
|
||||
"name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic",
|
||||
"version": "3.13.0-37-generic"
|
||||
},
|
||||
"created_at": "2014-11-14T16:36:31Z",
|
||||
"features": [
|
||||
"virtio"
|
||||
],
|
||||
"backup_ids": [
|
||||
|
||||
],
|
||||
"snapshot_ids": [
|
||||
7938206
|
||||
],
|
||||
"image": {
|
||||
"id": 6918990,
|
||||
"name": "14.04 x64",
|
||||
"distribution": "Ubuntu",
|
||||
"slug": "ubuntu-16-04-x64",
|
||||
"public": true,
|
||||
"regions": [
|
||||
"nyc1",
|
||||
"ams1",
|
||||
"sfo1",
|
||||
"nyc2",
|
||||
"ams2",
|
||||
"sgp1",
|
||||
"lon1",
|
||||
"nyc3",
|
||||
"ams3",
|
||||
"nyc3"
|
||||
],
|
||||
"created_at": "2014-10-17T20:24:33Z",
|
||||
"type": "snapshot",
|
||||
"min_disk_size": 20,
|
||||
"size_gigabytes": 2.34
|
||||
},
|
||||
"volume_ids": [
|
||||
|
||||
],
|
||||
"size": {
|
||||
},
|
||||
"size_slug": "s-1vcpu-1gb",
|
||||
"networks": {
|
||||
"v4": [
|
||||
{
|
||||
"ip_address": "104.131.186.241",
|
||||
"netmask": "255.255.240.0",
|
||||
"gateway": "104.131.176.1",
|
||||
"type": "public"
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"name": "New York 3",
|
||||
"slug": "nyc3",
|
||||
"sizes": [
|
||||
"s-1vcpu-1gb",
|
||||
"s-1vcpu-2gb",
|
||||
"s-1vcpu-3gb",
|
||||
"s-2vcpu-2gb",
|
||||
"s-3vcpu-1gb",
|
||||
"s-2vcpu-4gb",
|
||||
"s-4vcpu-8gb",
|
||||
"s-6vcpu-16gb",
|
||||
"s-8vcpu-32gb",
|
||||
"s-12vcpu-48gb",
|
||||
"s-16vcpu-64gb",
|
||||
"s-20vcpu-96gb",
|
||||
"s-24vcpu-128gb",
|
||||
"s-32vcpu-192gb"
|
||||
],
|
||||
"features": [
|
||||
"virtio",
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata"
|
||||
],
|
||||
"available": true
|
||||
},
|
||||
"tags": [
|
||||
"monitor"
|
||||
],
|
||||
"vpc_uuid": "f9b0769c-e118-42fb-a0c4-fed15ef69662"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"pages": {
|
||||
"next": "https://api.digitalocean.com/v2/droplets?page=2&per_page=2",
|
||||
"last": "https://api.digitalocean.com/v2/droplets?page=2&per_page=2"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 4
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
{
|
||||
"droplets": [
|
||||
{
|
||||
"id": 175072239,
|
||||
"name": "prometheus-demo-old",
|
||||
"memory": 1024,
|
||||
"vcpus": 1,
|
||||
"disk": 25,
|
||||
"locked": false,
|
||||
"status": "off",
|
||||
"kernel": null,
|
||||
"created_at": "2020-01-10T16:47:39Z",
|
||||
"features": [
|
||||
"ipv6",
|
||||
"private_networking"
|
||||
],
|
||||
"backup_ids": [],
|
||||
"next_backup_window": null,
|
||||
"snapshot_ids": [],
|
||||
"image": {
|
||||
"id": 53893572,
|
||||
"name": "18.04.3 (LTS) x64",
|
||||
"distribution": "Ubuntu",
|
||||
"slug": "ubuntu-18-04-x64",
|
||||
"public": true,
|
||||
"regions": [
|
||||
"nyc3",
|
||||
"nyc1",
|
||||
"sfo1",
|
||||
"nyc2",
|
||||
"ams2",
|
||||
"sgp1",
|
||||
"lon1",
|
||||
"nyc3",
|
||||
"ams3",
|
||||
"fra1",
|
||||
"tor1",
|
||||
"sfo2",
|
||||
"blr1",
|
||||
"sfo3"
|
||||
],
|
||||
"created_at": "2019-10-22T01:38:19Z",
|
||||
"min_disk_size": 20,
|
||||
"type": "base",
|
||||
"size_gigabytes": 2.36,
|
||||
"description": "Ubuntu 18.04 x64 20191022",
|
||||
"tags": [],
|
||||
"status": "available"
|
||||
},
|
||||
"volume_ids": [],
|
||||
"size": {
|
||||
"slug": "s-1vcpu-1gb",
|
||||
"memory": 1024,
|
||||
"vcpus": 1,
|
||||
"disk": 25,
|
||||
"transfer": 1,
|
||||
"price_monthly": 5,
|
||||
"price_hourly": 0.00744,
|
||||
"regions": [
|
||||
"ams2",
|
||||
"ams3",
|
||||
"blr1",
|
||||
"fra1",
|
||||
"lon1",
|
||||
"nyc1",
|
||||
"nyc2",
|
||||
"nyc3",
|
||||
"sfo1",
|
||||
"sfo2",
|
||||
"sfo3",
|
||||
"sgp1",
|
||||
"tor1"
|
||||
],
|
||||
"available": true
|
||||
},
|
||||
"size_slug": "s-1vcpu-1gb",
|
||||
"networks": {
|
||||
"v4": [
|
||||
{
|
||||
"ip_address": "167.172.111.118",
|
||||
"netmask": "255.255.240.0",
|
||||
"gateway": "167.172.176.1",
|
||||
"type": "public"
|
||||
},
|
||||
{
|
||||
"ip_address": "10.135.64.211",
|
||||
"netmask": "255.255.0.0",
|
||||
"gateway": "10.135.0.1",
|
||||
"type": "private"
|
||||
}
|
||||
],
|
||||
"v6": [
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"name": "Frankfurt 1",
|
||||
"slug": "fra1",
|
||||
"features": [
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata",
|
||||
"install_agent",
|
||||
"storage",
|
||||
"image_transfer"
|
||||
],
|
||||
"available": true,
|
||||
"sizes": [
|
||||
"s-1vcpu-1gb",
|
||||
"512mb",
|
||||
"s-1vcpu-2gb",
|
||||
"1gb",
|
||||
"s-3vcpu-1gb",
|
||||
"s-2vcpu-2gb",
|
||||
"s-1vcpu-3gb",
|
||||
"s-2vcpu-4gb",
|
||||
"2gb",
|
||||
"s-4vcpu-8gb",
|
||||
"m-1vcpu-8gb",
|
||||
"c-2",
|
||||
"4gb",
|
||||
"g-2vcpu-8gb",
|
||||
"gd-2vcpu-8gb",
|
||||
"m-16gb",
|
||||
"s-6vcpu-16gb",
|
||||
"c-4",
|
||||
"8gb",
|
||||
"m-2vcpu-16gb",
|
||||
"m3-2vcpu-16gb",
|
||||
"g-4vcpu-16gb",
|
||||
"gd-4vcpu-16gb",
|
||||
"m6-2vcpu-16gb",
|
||||
"m-32gb",
|
||||
"s-8vcpu-32gb",
|
||||
"c-8",
|
||||
"16gb",
|
||||
"m-4vcpu-32gb",
|
||||
"m3-4vcpu-32gb",
|
||||
"g-8vcpu-32gb",
|
||||
"s-12vcpu-48gb",
|
||||
"gd-8vcpu-32gb",
|
||||
"m6-4vcpu-32gb",
|
||||
"m-64gb",
|
||||
"s-16vcpu-64gb",
|
||||
"c-16",
|
||||
"32gb",
|
||||
"m-8vcpu-64gb",
|
||||
"m3-8vcpu-64gb",
|
||||
"g-16vcpu-64gb",
|
||||
"s-20vcpu-96gb",
|
||||
"48gb",
|
||||
"gd-16vcpu-64gb",
|
||||
"m6-8vcpu-64gb",
|
||||
"m-128gb",
|
||||
"s-24vcpu-128gb",
|
||||
"c-32",
|
||||
"64gb",
|
||||
"m-16vcpu-128gb",
|
||||
"m3-16vcpu-128gb",
|
||||
"s-32vcpu-192gb",
|
||||
"m-24vcpu-192gb",
|
||||
"m-224gb",
|
||||
"m6-16vcpu-128gb",
|
||||
"m3-24vcpu-192gb",
|
||||
"m6-24vcpu-192gb"
|
||||
]
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"id": 176011507,
|
||||
"name": "prometheus-demo",
|
||||
"memory": 1024,
|
||||
"vcpus": 1,
|
||||
"disk": 25,
|
||||
"locked": false,
|
||||
"status": "active",
|
||||
"kernel": null,
|
||||
"created_at": "2020-01-17T12:06:26Z",
|
||||
"features": [
|
||||
"ipv6",
|
||||
"private_networking"
|
||||
],
|
||||
"backup_ids": [],
|
||||
"next_backup_window": null,
|
||||
"snapshot_ids": [],
|
||||
"image": {
|
||||
"id": 53893572,
|
||||
"name": "18.04.3 (LTS) x64",
|
||||
"distribution": "Ubuntu",
|
||||
"slug": "ubuntu-18-04-x64",
|
||||
"public": true,
|
||||
"regions": [
|
||||
"nyc3",
|
||||
"nyc1",
|
||||
"sfo1",
|
||||
"nyc2",
|
||||
"ams2",
|
||||
"sgp1",
|
||||
"lon1",
|
||||
"nyc3",
|
||||
"ams3",
|
||||
"fra1",
|
||||
"tor1",
|
||||
"sfo2",
|
||||
"blr1",
|
||||
"sfo3"
|
||||
],
|
||||
"created_at": "2019-10-22T01:38:19Z",
|
||||
"min_disk_size": 20,
|
||||
"type": "base",
|
||||
"size_gigabytes": 2.36,
|
||||
"description": "Ubuntu 18.04 x64 20191022",
|
||||
"tags": [],
|
||||
"status": "available"
|
||||
},
|
||||
"volume_ids": [],
|
||||
"size": {
|
||||
"slug": "s-1vcpu-1gb",
|
||||
"memory": 1024,
|
||||
"vcpus": 1,
|
||||
"disk": 25,
|
||||
"transfer": 1,
|
||||
"price_monthly": 5,
|
||||
"price_hourly": 0.00744,
|
||||
"regions": [
|
||||
"ams2",
|
||||
"ams3",
|
||||
"blr1",
|
||||
"fra1",
|
||||
"lon1",
|
||||
"nyc1",
|
||||
"nyc2",
|
||||
"nyc3",
|
||||
"sfo1",
|
||||
"sfo2",
|
||||
"sfo3",
|
||||
"sgp1",
|
||||
"tor1"
|
||||
],
|
||||
"available": true
|
||||
},
|
||||
"size_slug": "s-1vcpu-1gb",
|
||||
"networks": {
|
||||
"v4": [
|
||||
{
|
||||
"ip_address": "138.65.56.69",
|
||||
"netmask": "255.255.240.0",
|
||||
"gateway": "138.65.64.1",
|
||||
"type": "public"
|
||||
},
|
||||
{
|
||||
"ip_address": "154.245.26.111",
|
||||
"netmask": "255.255.252.0",
|
||||
"gateway": "154.245.24.1",
|
||||
"type": "public"
|
||||
},
|
||||
{
|
||||
"ip_address": "10.135.64.212",
|
||||
"netmask": "255.255.0.0",
|
||||
"gateway": "10.135.0.1",
|
||||
"type": "private"
|
||||
}
|
||||
],
|
||||
"v6": [
|
||||
{
|
||||
"ip_address": "2a03:b0c0:3:f0::cf2:4",
|
||||
"netmask": 64,
|
||||
"gateway": "2a03:b0c0:3:f0::1",
|
||||
"type": "public"
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"name": "Frankfurt 1",
|
||||
"slug": "fra1",
|
||||
"features": [
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata",
|
||||
"install_agent",
|
||||
"storage",
|
||||
"image_transfer"
|
||||
],
|
||||
"available": true,
|
||||
"sizes": [
|
||||
"s-1vcpu-1gb",
|
||||
"512mb",
|
||||
"s-1vcpu-2gb",
|
||||
"1gb",
|
||||
"s-3vcpu-1gb",
|
||||
"s-2vcpu-2gb",
|
||||
"s-1vcpu-3gb",
|
||||
"s-2vcpu-4gb",
|
||||
"2gb",
|
||||
"s-4vcpu-8gb",
|
||||
"m-1vcpu-8gb",
|
||||
"c-2",
|
||||
"4gb",
|
||||
"g-2vcpu-8gb",
|
||||
"gd-2vcpu-8gb",
|
||||
"m-16gb",
|
||||
"s-6vcpu-16gb",
|
||||
"c-4",
|
||||
"8gb",
|
||||
"m-2vcpu-16gb",
|
||||
"m3-2vcpu-16gb",
|
||||
"g-4vcpu-16gb",
|
||||
"gd-4vcpu-16gb",
|
||||
"m6-2vcpu-16gb",
|
||||
"m-32gb",
|
||||
"s-8vcpu-32gb",
|
||||
"c-8",
|
||||
"16gb",
|
||||
"m-4vcpu-32gb",
|
||||
"m3-4vcpu-32gb",
|
||||
"g-8vcpu-32gb",
|
||||
"s-12vcpu-48gb",
|
||||
"gd-8vcpu-32gb",
|
||||
"m6-4vcpu-32gb",
|
||||
"m-64gb",
|
||||
"s-16vcpu-64gb",
|
||||
"c-16",
|
||||
"32gb",
|
||||
"m-8vcpu-64gb",
|
||||
"m3-8vcpu-64gb",
|
||||
"g-16vcpu-64gb",
|
||||
"s-20vcpu-96gb",
|
||||
"48gb",
|
||||
"gd-16vcpu-64gb",
|
||||
"m6-8vcpu-64gb",
|
||||
"m-128gb",
|
||||
"s-24vcpu-128gb",
|
||||
"c-32",
|
||||
"64gb",
|
||||
"m-16vcpu-128gb",
|
||||
"m3-16vcpu-128gb",
|
||||
"s-32vcpu-192gb",
|
||||
"m-24vcpu-192gb",
|
||||
"m-224gb",
|
||||
"m6-16vcpu-128gb",
|
||||
"m3-24vcpu-192gb",
|
||||
"m6-24vcpu-192gb"
|
||||
]
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"pages": {
|
||||
"first": "https://api.digitalocean.com/v2/droplets?page=1&per_page=2",
|
||||
"prev": "https://api.digitalocean.com/v2/droplets?page=1&per_page=2"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 4
|
||||
}
|
||||
}
|
||||
`,
|
||||
}[page-1],
|
||||
)
|
||||
})
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
# A example scrape configuration for running Prometheus with
|
||||
# DigitalOcean.
|
||||
|
||||
scrape_configs:
|
||||
|
||||
# Make Prometheus scrape itself for metrics.
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
# Discover Node Exporter instances to scrape.
|
||||
- job_name: 'node'
|
||||
|
||||
digitalocean_sd_configs:
|
||||
- bearer_token: "<replace with a Personal Access Token>"
|
||||
relabel_configs:
|
||||
# Only scrape targets that have a tag 'monitoring'.
|
||||
- source_labels: [__meta_digitalocean_tags]
|
||||
regex: '.*,monitoring,.*'
|
||||
action: keep
|
||||
|
||||
# Use the public IPv6 address and port 9100 to scrape the target.
|
||||
- source_labels: [__meta_digitalocean_public_ipv6]
|
||||
target_label: __address__
|
||||
replacement: '[$1]:9100'
|
@ -0,0 +1 @@
|
||||
vendor/
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"checkRunSettings": {
|
||||
"vulnerableCheckRunConclusionLevel": "failure"
|
||||
},
|
||||
"issueSettings": {
|
||||
"minSeverityLevel": "LOW"
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const oneClickBasePath = "v2/1-clicks"
|
||||
|
||||
// OneClickService is an interface for interacting with 1-clicks with the
|
||||
// DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#1-click-applications
|
||||
type OneClickService interface {
|
||||
List(context.Context, string) ([]*OneClick, *Response, error)
|
||||
}
|
||||
|
||||
var _ OneClickService = &OneClickServiceOp{}
|
||||
|
||||
// OneClickServiceOp interfaces with 1-click endpoints in the DigitalOcean API.
|
||||
type OneClickServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// OneClick is the structure of a 1-click
|
||||
type OneClick struct {
|
||||
Slug string `json:"slug"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// OneClicksRoot is the root of the json payload that contains a list of 1-clicks
|
||||
type OneClicksRoot struct {
|
||||
List []*OneClick `json:"1_clicks"`
|
||||
}
|
||||
|
||||
// List returns a list of the available 1-click applications.
|
||||
func (ocs *OneClickServiceOp) List(ctx context.Context, oneClickType string) ([]*OneClick, *Response, error) {
|
||||
path := fmt.Sprintf(`%s?type=%s`, oneClickBasePath, oneClickType)
|
||||
|
||||
req, err := ocs.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(OneClicksRoot)
|
||||
resp, err := ocs.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.List, resp, nil
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
# Change Log
|
||||
|
||||
## [v1.37.0] - 2020-06-01
|
||||
|
||||
- #336 registry: URL encode repository names when building URLs. @adamwg
|
||||
- #335 Add 1-click service and request. @scottcrawford03
|
||||
|
||||
## [v1.36.0] - 2020-05-12
|
||||
|
||||
- #331 Expose expiry_seconds for Registry.DockerCredentials. @andrewsomething
|
||||
|
||||
## [v1.35.1] - 2020-04-21
|
||||
|
||||
- #328 Update vulnerable x/crypto dependency - @bentranter
|
||||
|
||||
## [v1.35.0] - 2020-04-20
|
||||
|
||||
- #326 Add TagCount field to registry/Repository - @nicktate
|
||||
- #325 Add DOCR EA routes - @nicktate
|
||||
- #324 Upgrade godo to Go 1.14 - @bentranter
|
||||
|
||||
## [v1.34.0] - 2020-03-30
|
||||
|
||||
- #320 Add VPC v3 attributes - @viola
|
||||
|
||||
## [v1.33.1] - 2020-03-23
|
||||
|
||||
- #318 upgrade github.com/stretchr/objx past 0.1.1 - @hilary
|
||||
|
||||
## [v1.33.0] - 2020-03-20
|
||||
|
||||
- #310 Add BillingHistory service and List endpoint - @rbutler
|
||||
- #316 load balancers: add new enable_backend_keepalive field - @anitgandhi
|
||||
|
||||
## [v1.32.0] - 2020-03-04
|
||||
|
||||
- #311 Add reset database user auth method - @zbarahal-do
|
||||
|
||||
## [v1.31.0] - 2020-02-28
|
||||
|
||||
- #305 invoices: GetPDF and GetCSV methods - @rbutler
|
||||
- #304 Add NewFromToken convenience method to init client - @bentranter
|
||||
- #301 invoices: Get, Summary, and List methods - @rbutler
|
||||
- #299 Fix param expiry_seconds for kubernetes.GetCredentials request - @velp
|
||||
|
||||
## [v1.30.0] - 2020-02-03
|
||||
|
||||
- #295 registry: support the created_at field - @adamwg
|
||||
- #293 doks: node pool labels - @snormore
|
||||
|
||||
## [v1.29.0] - 2019-12-13
|
||||
|
||||
- #288 Add Balance Get method - @rbutler
|
||||
- #286,#289 Deserialize meta field - @timoreimann
|
||||
|
||||
## [v1.28.0] - 2019-12-04
|
||||
|
||||
- #282 Add valid Redis eviction policy constants - @bentranter
|
||||
- #281 Remove databases info from top-level godoc string - @bentranter
|
||||
- #280 Fix VolumeSnapshotResourceType value volumesnapshot -> volume_snapshot - @aqche
|
||||
|
||||
## [v1.27.0] - 2019-11-18
|
||||
|
||||
- #278 add mysql user auth settings for database users - @gregmankes
|
||||
|
||||
## [v1.26.0] - 2019-11-13
|
||||
|
||||
- #272 dbaas: get and set mysql sql mode - @mikejholly
|
||||
|
||||
## [v1.25.0] - 2019-11-13
|
||||
|
||||
- #275 registry/docker-credentials: add support for the read/write parameter - @kamaln7
|
||||
- #273 implement the registry/docker-credentials endpoint - @kamaln7
|
||||
- #271 Add registry resource - @snormore
|
||||
|
||||
## [v1.24.1] - 2019-11-04
|
||||
|
||||
- #264 Update isLast to check p.Next - @aqche
|
||||
|
||||
## [v1.24.0] - 2019-10-30
|
||||
|
||||
- #267 Return []DatabaseFirewallRule in addition to raw response. - @andrewsomething
|
||||
|
||||
## [v1.23.1] - 2019-10-30
|
||||
|
||||
- #265 add support for getting/setting firewall rules - @gregmankes
|
||||
- #262 remove ResolveReference call - @mdanzinger
|
||||
- #261 Update CONTRIBUTING.md - @mdanzinger
|
||||
|
||||
## [v1.22.0] - 2019-09-24
|
||||
|
||||
- #259 Add Kubernetes GetCredentials method - @snormore
|
||||
|
||||
## [v1.21.1] - 2019-09-19
|
||||
|
||||
- #257 Upgrade to Go 1.13 - @bentranter
|
||||
|
||||
## [v1.21.0] - 2019-09-16
|
||||
|
||||
- #255 Add DropletID to Kubernetes Node instance - @snormore
|
||||
- #254 Add tags to Database, DatabaseReplica - @Zyqsempai
|
||||
|
||||
## [v1.20.0] - 2019-09-06
|
||||
|
||||
- #252 Add Kubernetes autoscale config fields - @snormore
|
||||
- #251 Support unset fields on Kubernetes cluster and node pool updates - @snormore
|
||||
- #250 Add Kubernetes GetUser method - @snormore
|
||||
|
||||
## [v1.19.0] - 2019-07-19
|
||||
|
||||
- #244 dbaas: add private-network-uuid field to create request
|
||||
|
||||
## [v1.18.0] - 2019-07-17
|
||||
|
||||
- #241 Databases: support for custom VPC UUID on migrate @mikejholly
|
||||
- #240 Add the ability to get URN for a Database @stack72
|
||||
- #236 Fix omitempty typos in JSON struct tags @amccarthy1
|
||||
|
||||
## [v1.17.0] - 2019-06-21
|
||||
|
||||
- #238 Add support for Redis eviction policy in Databases @mikejholly
|
||||
|
||||
## [v1.16.0] - 2019-06-04
|
||||
|
||||
- #233 Add Kubernetes DeleteNode method, deprecate RecycleNodePoolNodes @bouk
|
||||
|
||||
## [v1.15.0] - 2019-05-13
|
||||
|
||||
- #231 Add private connection fields to Databases - @mikejholly
|
||||
- #223 Introduce Go modules - @andreiavrammsd
|
||||
|
||||
## [v1.14.0] - 2019-05-13
|
||||
|
||||
- #229 Add support for upgrading Kubernetes clusters - @adamwg
|
||||
|
||||
## [v1.13.0] - 2019-04-19
|
||||
|
||||
- #213 Add tagging support for volume snapshots - @jcodybaker
|
||||
|
||||
## [v1.12.0] - 2019-04-18
|
||||
|
||||
- #224 Add maintenance window support for Kubernetes- @fatih
|
||||
|
||||
## [v1.11.1] - 2019-04-04
|
||||
|
||||
- #222 Fix Create Database Pools json fields - @sunny-b
|
||||
|
||||
## [v1.11.0] - 2019-04-03
|
||||
|
||||
- #220 roll out vpc functionality - @jheimann
|
||||
|
||||
## [v1.10.1] - 2019-03-27
|
||||
|
||||
- #219 Fix Database Pools json field - @sunny-b
|
||||
|
||||
## [v1.10.0] - 2019-03-20
|
||||
|
||||
- #215 Add support for Databases - @mikejholly
|
||||
|
||||
## [v1.9.0] - 2019-03-18
|
||||
|
||||
- #214 add support for enable_proxy_protocol. - @mregmi
|
||||
|
||||
## [v1.8.0] - 2019-03-13
|
||||
|
||||
- #210 Expose tags on storage volume create/list/get. - @jcodybaker
|
||||
|
||||
## [v1.7.5] - 2019-03-04
|
||||
|
||||
- #207 Add support for custom subdomains for Spaces CDN [beta] - @xornivore
|
||||
|
||||
## [v1.7.4] - 2019-02-08
|
||||
|
||||
- #202 Allow tagging volumes - @mchitten
|
||||
|
||||
## [v1.7.3] - 2018-12-18
|
||||
|
||||
- #196 Expose tag support for creating Load Balancers.
|
||||
|
||||
## [v1.7.2] - 2018-12-04
|
||||
|
||||
- #192 Exposes more options for Kubernetes clusters.
|
||||
|
||||
## [v1.7.1] - 2018-11-27
|
||||
|
||||
- #190 Expose constants for the state of Kubernetes clusters.
|
||||
|
||||
## [v1.7.0] - 2018-11-13
|
||||
|
||||
- #188 Kubernetes support [beta] - @aybabtme
|
||||
|
||||
## [v1.6.0] - 2018-10-16
|
||||
|
||||
- #185 Projects support [beta] - @mchitten
|
||||
|
||||
## [v1.5.0] - 2018-10-01
|
||||
|
||||
- #181 Adding tagging images support - @hugocorbucci
|
||||
|
||||
## [v1.4.2] - 2018-08-30
|
||||
|
||||
- #178 Allowing creating domain records with weight of 0 - @TFaga
|
||||
- #177 Adding `VolumeLimit` to account - @lxfontes
|
||||
|
||||
## [v1.4.1] - 2018-08-23
|
||||
|
||||
- #176 Fix cdn flush cache API endpoint - @sunny-b
|
||||
|
||||
## [v1.4.0] - 2018-08-22
|
||||
|
||||
- #175 Add support for Spaces CDN - @sunny-b
|
||||
|
||||
## [v1.3.0] - 2018-05-24
|
||||
|
||||
- #170 Add support for volume formatting - @adamwg
|
||||
|
||||
## [v1.2.0] - 2018-05-08
|
||||
|
||||
- #166 Remove support for Go 1.6 - @iheanyi
|
||||
- #165 Add support for Let's Encrypt Certificates - @viola
|
||||
|
||||
## [v1.1.3] - 2018-03-07
|
||||
|
||||
- #156 Handle non-json errors from the API - @aknuds1
|
||||
- #158 Update droplet example to use latest instance type - @dan-v
|
||||
|
||||
## [v1.1.2] - 2018-03-06
|
||||
|
||||
- #157 storage: list volumes should handle only name or only region params - @andrewsykim
|
||||
- #154 docs: replace first example with fully-runnable example - @xmudrii
|
||||
- #152 Handle flags & tag properties of domain record - @jaymecd
|
||||
|
||||
## [v1.1.1] - 2017-09-29
|
||||
|
||||
- #151 Following user agent field recommendations - @joonas
|
||||
- #148 AsRequest method to create load balancers requests - @lukegb
|
||||
|
||||
## [v1.1.0] - 2017-06-06
|
||||
|
||||
### Added
|
||||
- #145 Add FirewallsService for managing Firewalls with the DigitalOcean API. - @viola
|
||||
- #139 Add TTL field to the Domains. - @xmudrii
|
||||
|
||||
### Fixed
|
||||
- #143 Fix oauth2.NoContext depreciation. - @jbowens
|
||||
- #141 Fix DropletActions on tagged resources. - @xmudrii
|
||||
|
||||
## [v1.0.0] - 2017-03-10
|
||||
|
||||
### Added
|
||||
- #130 Add Convert to ImageActionsService. - @xmudrii
|
||||
- #126 Add CertificatesService for managing certificates with the DigitalOcean API. - @viola
|
||||
- #125 Add LoadBalancersService for managing load balancers with the DigitalOcean API. - @viola
|
||||
- #122 Add GetVolumeByName to StorageService. - @protochron
|
||||
- #113 Add context.Context to all calls. - @aybabtme
|
@ -0,0 +1,54 @@
|
||||
# Contributing
|
||||
|
||||
We love contributions! You are welcome to open a pull request, but it's a good idea to
|
||||
open an issue and discuss your idea with us first.
|
||||
|
||||
Once you are ready to open a PR, please keep the following guidelines in mind:
|
||||
|
||||
1. Code should be `go fmt` compliant.
|
||||
1. Types, structs and funcs should be documented.
|
||||
1. Tests pass.
|
||||
|
||||
## Getting set up
|
||||
|
||||
`godo` uses go modules. Just fork this repo, clone your fork and off you go!
|
||||
|
||||
## Running tests
|
||||
|
||||
When working on code in this repository, tests can be run via:
|
||||
|
||||
```sh
|
||||
go test -mod=vendor .
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
Godo follows [semver](https://www.semver.org) versioning semantics.
|
||||
New functionality should be accompanied by increment to the minor
|
||||
version number. Any code merged to master is subject to release.
|
||||
|
||||
## Releasing
|
||||
|
||||
Releasing a new version of godo is currently a manual process.
|
||||
|
||||
Submit a separate pull request for the version change from the pull
|
||||
request with your changes.
|
||||
|
||||
1. Update the `CHANGELOG.md` with your changes. If a version header
|
||||
for the next (unreleased) version does not exist, create one.
|
||||
Include one bullet point for each piece of new functionality in the
|
||||
release, including the pull request ID, description, and author(s).
|
||||
|
||||
```
|
||||
## [v1.8.0] - 2019-03-13
|
||||
|
||||
- #210 Expose tags on storage volume create/list/get. - @jcodybaker
|
||||
- #123 Update test dependencies - @digitalocean
|
||||
```
|
||||
|
||||
2. Update the `libraryVersion` number in `godo.go`.
|
||||
3. Make a pull request with these changes. This PR should be separate from the PR containing the godo changes.
|
||||
4. Once the pull request has been merged, [draft a new release](https://github.com/digitalocean/godo/releases/new).
|
||||
5. Update the `Tag version` and `Release title` field with the new godo version. Be sure the version has a `v` prefixed in both places. Ex `v1.8.0`.
|
||||
6. Copy the changelog bullet points to the description field.
|
||||
7. Publish the release.
|
@ -0,0 +1,55 @@
|
||||
Copyright (c) 2014-2016 The godo AUTHORS. All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
======================
|
||||
Portions of the client are based on code at:
|
||||
https://github.com/google/go-github/
|
||||
|
||||
Copyright (c) 2013 The go-github AUTHORS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
@ -0,0 +1,139 @@
|
||||
# Godo
|
||||
|
||||
[![Build Status](https://travis-ci.org/digitalocean/godo.svg)](https://travis-ci.org/digitalocean/godo)
|
||||
[![GoDoc](https://godoc.org/github.com/digitalocean/godo?status.svg)](https://godoc.org/github.com/digitalocean/godo)
|
||||
|
||||
Godo is a Go client library for accessing the DigitalOcean V2 API.
|
||||
|
||||
You can view the client API docs here: [http://godoc.org/github.com/digitalocean/godo](http://godoc.org/github.com/digitalocean/godo)
|
||||
|
||||
You can view DigitalOcean API docs here: [https://developers.digitalocean.com/documentation/v2/](https://developers.digitalocean.com/documentation/v2/)
|
||||
|
||||
## Install
|
||||
```sh
|
||||
go get github.com/digitalocean/godo@vX.Y.Z
|
||||
```
|
||||
|
||||
where X.Y.Z is the [version](https://github.com/digitalocean/godo/releases) you need.
|
||||
|
||||
or
|
||||
```sh
|
||||
go get github.com/digitalocean/godo
|
||||
```
|
||||
for non Go modules usage or latest version.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/digitalocean/godo"
|
||||
```
|
||||
|
||||
Create a new DigitalOcean client, then use the exposed services to
|
||||
access different parts of the DigitalOcean API.
|
||||
|
||||
### Authentication
|
||||
|
||||
Currently, Personal Access Token (PAT) is the only method of
|
||||
authenticating with the API. You can manage your tokens
|
||||
at the DigitalOcean Control Panel [Applications Page](https://cloud.digitalocean.com/settings/applications).
|
||||
|
||||
You can then use your token to create a new client:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/digitalocean/godo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := godo.NewFromToken("my-digitalocean-api-token")
|
||||
}
|
||||
```
|
||||
|
||||
If you need to provide a `context.Context` to your new client, you should use [`godo.NewClient`](https://godoc.org/github.com/digitalocean/godo#NewClient) to manually construct a client instead.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
To create a new Droplet:
|
||||
|
||||
```go
|
||||
dropletName := "super-cool-droplet"
|
||||
|
||||
createRequest := &godo.DropletCreateRequest{
|
||||
Name: dropletName,
|
||||
Region: "nyc3",
|
||||
Size: "s-1vcpu-1gb",
|
||||
Image: godo.DropletCreateImage{
|
||||
Slug: "ubuntu-14-04-x64",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
newDroplet, _, err := client.Droplets.Create(ctx, createRequest)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Something bad happened: %s\n\n", err)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
If a list of items is paginated by the API, you must request pages individually. For example, to fetch all Droplets:
|
||||
|
||||
```go
|
||||
func DropletList(ctx context.Context, client *godo.Client) ([]godo.Droplet, error) {
|
||||
// create a list to hold our droplets
|
||||
list := []godo.Droplet{}
|
||||
|
||||
// create options. initially, these will be blank
|
||||
opt := &godo.ListOptions{}
|
||||
for {
|
||||
droplets, resp, err := client.Droplets.List(ctx, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// append the current page's droplets to our list
|
||||
for _, d := range droplets {
|
||||
list = append(list, d)
|
||||
}
|
||||
|
||||
// if we are at the last page, break out the for loop
|
||||
if resp.Links == nil || resp.Links.IsLastPage() {
|
||||
break
|
||||
}
|
||||
|
||||
page, err := resp.Links.CurrentPage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set the page we want for the next request
|
||||
opt.Page = page + 1
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
Each version of the client is tagged and the version is updated accordingly.
|
||||
|
||||
To see the list of past versions, run `git tag`.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
For a comprehensive list of examples, check out the [API documentation](https://developers.digitalocean.com/documentation/v2/).
|
||||
|
||||
For details on all the functionality in this library, see the [GoDoc](http://godoc.org/github.com/digitalocean/godo) documentation.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
We love pull requests! Please see the [contribution guidelines](CONTRIBUTING.md).
|
@ -0,0 +1,60 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AccountService is an interface for interfacing with the Account
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#account
|
||||
type AccountService interface {
|
||||
Get(context.Context) (*Account, *Response, error)
|
||||
}
|
||||
|
||||
// AccountServiceOp handles communication with the Account related methods of
|
||||
// the DigitalOcean API.
|
||||
type AccountServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ AccountService = &AccountServiceOp{}
|
||||
|
||||
// Account represents a DigitalOcean Account
|
||||
type Account struct {
|
||||
DropletLimit int `json:"droplet_limit,omitempty"`
|
||||
FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
|
||||
VolumeLimit int `json:"volume_limit,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusMessage string `json:"status_message,omitempty"`
|
||||
}
|
||||
|
||||
type accountRoot struct {
|
||||
Account *Account `json:"account"`
|
||||
}
|
||||
|
||||
func (r Account) String() string {
|
||||
return Stringify(r)
|
||||
}
|
||||
|
||||
// Get DigitalOcean account info
|
||||
func (s *AccountServiceOp) Get(ctx context.Context) (*Account, *Response, error) {
|
||||
|
||||
path := "v2/account"
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(accountRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Account, resp, err
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
actionsBasePath = "v2/actions"
|
||||
|
||||
// ActionInProgress is an in progress action status
|
||||
ActionInProgress = "in-progress"
|
||||
|
||||
//ActionCompleted is a completed action status
|
||||
ActionCompleted = "completed"
|
||||
)
|
||||
|
||||
// ActionsService handles communction with action related methods of the
|
||||
// DigitalOcean API: https://developers.digitalocean.com/documentation/v2#actions
|
||||
type ActionsService interface {
|
||||
List(context.Context, *ListOptions) ([]Action, *Response, error)
|
||||
Get(context.Context, int) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// ActionsServiceOp handles communition with the image action related methods of the
|
||||
// DigitalOcean API.
|
||||
type ActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ ActionsService = &ActionsServiceOp{}
|
||||
|
||||
type actionsRoot struct {
|
||||
Actions []Action `json:"actions"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type actionRoot struct {
|
||||
Event *Action `json:"action"`
|
||||
}
|
||||
|
||||
// Action represents a DigitalOcean Action
|
||||
type Action struct {
|
||||
ID int `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
StartedAt *Timestamp `json:"started_at"`
|
||||
CompletedAt *Timestamp `json:"completed_at"`
|
||||
ResourceID int `json:"resource_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Region *Region `json:"region,omitempty"`
|
||||
RegionSlug string `json:"region_slug,omitempty"`
|
||||
}
|
||||
|
||||
// List all actions
|
||||
func (s *ActionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Action, *Response, error) {
|
||||
path := actionsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
// Get an action by ID.
|
||||
func (s *ActionsServiceOp) Get(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", actionsBasePath, id)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (a Action) String() string {
|
||||
return Stringify(a)
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BalanceService is an interface for interfacing with the Balance
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#balance
|
||||
type BalanceService interface {
|
||||
Get(context.Context) (*Balance, *Response, error)
|
||||
}
|
||||
|
||||
// BalanceServiceOp handles communication with the Balance related methods of
|
||||
// the DigitalOcean API.
|
||||
type BalanceServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ BalanceService = &BalanceServiceOp{}
|
||||
|
||||
// Balance represents a DigitalOcean Balance
|
||||
type Balance struct {
|
||||
MonthToDateBalance string `json:"month_to_date_balance"`
|
||||
AccountBalance string `json:"account_balance"`
|
||||
MonthToDateUsage string `json:"month_to_date_usage"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
func (r Balance) String() string {
|
||||
return Stringify(r)
|
||||
}
|
||||
|
||||
// Get DigitalOcean balance info
|
||||
func (s *BalanceServiceOp) Get(ctx context.Context) (*Balance, *Response, error) {
|
||||
path := "v2/customers/my/balance"
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Balance)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, err
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const billingHistoryBasePath = "v2/customers/my/billing_history"
|
||||
|
||||
// BillingHistoryService is an interface for interfacing with the BillingHistory
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#billing_history
|
||||
type BillingHistoryService interface {
|
||||
List(context.Context, *ListOptions) (*BillingHistory, *Response, error)
|
||||
}
|
||||
|
||||
// BillingHistoryServiceOp handles communication with the BillingHistory related methods of
|
||||
// the DigitalOcean API.
|
||||
type BillingHistoryServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ BillingHistoryService = &BillingHistoryServiceOp{}
|
||||
|
||||
// BillingHistory represents a DigitalOcean Billing History
|
||||
type BillingHistory struct {
|
||||
BillingHistory []BillingHistoryEntry `json:"billing_history"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// BillingHistoryEntry represents an entry in a customer's Billing History
|
||||
type BillingHistoryEntry struct {
|
||||
Description string `json:"description"`
|
||||
Amount string `json:"amount"`
|
||||
InvoiceID *string `json:"invoice_id"`
|
||||
InvoiceUUID *string `json:"invoice_uuid"`
|
||||
Date time.Time `json:"date"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (b BillingHistory) String() string {
|
||||
return Stringify(b)
|
||||
}
|
||||
|
||||
// List the Billing History for a customer
|
||||
func (s *BillingHistoryServiceOp) List(ctx context.Context, opt *ListOptions) (*BillingHistory, *Response, error) {
|
||||
path, err := addOptions(billingHistoryBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(BillingHistory)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root, resp, err
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const cdnBasePath = "v2/cdn/endpoints"
|
||||
|
||||
// CDNService is an interface for managing Spaces CDN with the DigitalOcean API.
|
||||
type CDNService interface {
|
||||
List(context.Context, *ListOptions) ([]CDN, *Response, error)
|
||||
Get(context.Context, string) (*CDN, *Response, error)
|
||||
Create(context.Context, *CDNCreateRequest) (*CDN, *Response, error)
|
||||
UpdateTTL(context.Context, string, *CDNUpdateTTLRequest) (*CDN, *Response, error)
|
||||
UpdateCustomDomain(context.Context, string, *CDNUpdateCustomDomainRequest) (*CDN, *Response, error)
|
||||
FlushCache(context.Context, string, *CDNFlushCacheRequest) (*Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// CDNServiceOp handles communication with the CDN related methods of the
|
||||
// DigitalOcean API.
|
||||
type CDNServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ CDNService = &CDNServiceOp{}
|
||||
|
||||
// CDN represents a DigitalOcean CDN
|
||||
type CDN struct {
|
||||
ID string `json:"id"`
|
||||
Origin string `json:"origin"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
CertificateID string `json:"certificate_id,omitempty"`
|
||||
CustomDomain string `json:"custom_domain,omitempty"`
|
||||
}
|
||||
|
||||
// CDNRoot represents a response from the DigitalOcean API
|
||||
type cdnRoot struct {
|
||||
Endpoint *CDN `json:"endpoint"`
|
||||
}
|
||||
|
||||
type cdnsRoot struct {
|
||||
Endpoints []CDN `json:"endpoints"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// CDNCreateRequest represents a request to create a CDN.
|
||||
type CDNCreateRequest struct {
|
||||
Origin string `json:"origin"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
CustomDomain string `json:"custom_domain,omitempty"`
|
||||
CertificateID string `json:"certificate_id,omitempty"`
|
||||
}
|
||||
|
||||
// CDNUpdateTTLRequest represents a request to update the ttl of a CDN.
|
||||
type CDNUpdateTTLRequest struct {
|
||||
TTL uint32 `json:"ttl"`
|
||||
}
|
||||
|
||||
// CDNUpdateCustomDomainRequest represents a request to update the custom domain of a CDN.
|
||||
type CDNUpdateCustomDomainRequest struct {
|
||||
CustomDomain string `json:"custom_domain"`
|
||||
CertificateID string `json:"certificate_id"`
|
||||
}
|
||||
|
||||
// CDNFlushCacheRequest represents a request to flush cache of a CDN.
|
||||
type CDNFlushCacheRequest struct {
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
// List all CDN endpoints
|
||||
func (c CDNServiceOp) List(ctx context.Context, opt *ListOptions) ([]CDN, *Response, error) {
|
||||
path, err := addOptions(cdnBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(cdnsRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Endpoints, resp, err
|
||||
}
|
||||
|
||||
// Get individual CDN. It requires a non-empty cdn id.
|
||||
func (c CDNServiceOp) Get(ctx context.Context, id string) (*CDN, *Response, error) {
|
||||
if len(id) == 0 {
|
||||
return nil, nil, NewArgError("id", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", cdnBasePath, id)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(cdnRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Endpoint, resp, err
|
||||
}
|
||||
|
||||
// Create a new CDN
|
||||
func (c CDNServiceOp) Create(ctx context.Context, createRequest *CDNCreateRequest) (*CDN, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodPost, cdnBasePath, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(cdnRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Endpoint, resp, err
|
||||
}
|
||||
|
||||
// UpdateTTL updates the ttl of an individual CDN
|
||||
func (c CDNServiceOp) UpdateTTL(ctx context.Context, id string, updateRequest *CDNUpdateTTLRequest) (*CDN, *Response, error) {
|
||||
return c.update(ctx, id, updateRequest)
|
||||
}
|
||||
|
||||
// UpdateCustomDomain sets or removes the custom domain of an individual CDN
|
||||
func (c CDNServiceOp) UpdateCustomDomain(ctx context.Context, id string, updateRequest *CDNUpdateCustomDomainRequest) (*CDN, *Response, error) {
|
||||
return c.update(ctx, id, updateRequest)
|
||||
}
|
||||
|
||||
func (c CDNServiceOp) update(ctx context.Context, id string, updateRequest interface{}) (*CDN, *Response, error) {
|
||||
if updateRequest == nil {
|
||||
return nil, nil, NewArgError("updateRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
if len(id) == 0 {
|
||||
return nil, nil, NewArgError("id", "cannot be an empty string")
|
||||
}
|
||||
path := fmt.Sprintf("%s/%s", cdnBasePath, id)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodPut, path, updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(cdnRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Endpoint, resp, err
|
||||
}
|
||||
|
||||
// FlushCache flushes the cache of an individual CDN. Requires a non-empty slice of file paths and/or wildcards
|
||||
func (c CDNServiceOp) FlushCache(ctx context.Context, id string, flushCacheRequest *CDNFlushCacheRequest) (*Response, error) {
|
||||
if flushCacheRequest == nil {
|
||||
return nil, NewArgError("flushCacheRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
if len(id) == 0 {
|
||||
return nil, NewArgError("id", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/cache", cdnBasePath, id)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodDelete, path, flushCacheRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Delete an individual CDN
|
||||
func (c CDNServiceOp) Delete(ctx context.Context, id string) (*Response, error) {
|
||||
if len(id) == 0 {
|
||||
return nil, NewArgError("id", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", cdnBasePath, id)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path"
|
||||
)
|
||||
|
||||
const certificatesBasePath = "/v2/certificates"
|
||||
|
||||
// CertificatesService is an interface for managing certificates with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#certificates
|
||||
type CertificatesService interface {
|
||||
Get(context.Context, string) (*Certificate, *Response, error)
|
||||
List(context.Context, *ListOptions) ([]Certificate, *Response, error)
|
||||
Create(context.Context, *CertificateRequest) (*Certificate, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// Certificate represents a DigitalOcean certificate configuration.
|
||||
type Certificate struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DNSNames []string `json:"dns_names,omitempty"`
|
||||
NotAfter string `json:"not_after,omitempty"`
|
||||
SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// CertificateRequest represents configuration for a new certificate.
|
||||
type CertificateRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
DNSNames []string `json:"dns_names,omitempty"`
|
||||
PrivateKey string `json:"private_key,omitempty"`
|
||||
LeafCertificate string `json:"leaf_certificate,omitempty"`
|
||||
CertificateChain string `json:"certificate_chain,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type certificateRoot struct {
|
||||
Certificate *Certificate `json:"certificate"`
|
||||
}
|
||||
|
||||
type certificatesRoot struct {
|
||||
Certificates []Certificate `json:"certificates"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// CertificatesServiceOp handles communication with certificates methods of the DigitalOcean API.
|
||||
type CertificatesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ CertificatesService = &CertificatesServiceOp{}
|
||||
|
||||
// Get an existing certificate by its identifier.
|
||||
func (c *CertificatesServiceOp) Get(ctx context.Context, cID string) (*Certificate, *Response, error) {
|
||||
urlStr := path.Join(certificatesBasePath, cID)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(certificateRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Certificate, resp, nil
|
||||
}
|
||||
|
||||
// List all certificates.
|
||||
func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Certificate, *Response, error) {
|
||||
urlStr, err := addOptions(certificatesBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(certificatesRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Certificates, resp, nil
|
||||
}
|
||||
|
||||
// Create a new certificate with provided configuration.
|
||||
func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateRequest) (*Certificate, *Response, error) {
|
||||
req, err := c.client.NewRequest(ctx, http.MethodPost, certificatesBasePath, cr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(certificateRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Certificate, resp, nil
|
||||
}
|
||||
|
||||
// Delete a certificate by its identifier.
|
||||
func (c *CertificatesServiceOp) Delete(ctx context.Context, cID string) (*Response, error) {
|
||||
urlStr := path.Join(certificatesBasePath, cID)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodDelete, urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.client.Do(ctx, req, nil)
|
||||
}
|
@ -0,0 +1,845 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
databaseBasePath = "/v2/databases"
|
||||
databaseSinglePath = databaseBasePath + "/%s"
|
||||
databaseResizePath = databaseBasePath + "/%s/resize"
|
||||
databaseMigratePath = databaseBasePath + "/%s/migrate"
|
||||
databaseMaintenancePath = databaseBasePath + "/%s/maintenance"
|
||||
databaseBackupsPath = databaseBasePath + "/%s/backups"
|
||||
databaseUsersPath = databaseBasePath + "/%s/users"
|
||||
databaseUserPath = databaseBasePath + "/%s/users/%s"
|
||||
databaseResetUserAuthPath = databaseUserPath + "/reset_auth"
|
||||
databaseDBPath = databaseBasePath + "/%s/dbs/%s"
|
||||
databaseDBsPath = databaseBasePath + "/%s/dbs"
|
||||
databasePoolPath = databaseBasePath + "/%s/pools/%s"
|
||||
databasePoolsPath = databaseBasePath + "/%s/pools"
|
||||
databaseReplicaPath = databaseBasePath + "/%s/replicas/%s"
|
||||
databaseReplicasPath = databaseBasePath + "/%s/replicas"
|
||||
databaseEvictionPolicyPath = databaseBasePath + "/%s/eviction_policy"
|
||||
databaseSQLModePath = databaseBasePath + "/%s/sql_mode"
|
||||
databaseFirewallRulesPath = databaseBasePath + "/%s/firewall"
|
||||
)
|
||||
|
||||
// SQL Mode constants allow for MySQL-specific SQL flavor configuration.
|
||||
const (
|
||||
SQLModeAllowInvalidDates = "ALLOW_INVALID_DATES"
|
||||
SQLModeANSIQuotes = "ANSI_QUOTES"
|
||||
SQLModeHighNotPrecedence = "HIGH_NOT_PRECEDENCE"
|
||||
SQLModeIgnoreSpace = "IGNORE_SPACE"
|
||||
SQLModeNoAuthCreateUser = "NO_AUTO_CREATE_USER"
|
||||
SQLModeNoAutoValueOnZero = "NO_AUTO_VALUE_ON_ZERO"
|
||||
SQLModeNoBackslashEscapes = "NO_BACKSLASH_ESCAPES"
|
||||
SQLModeNoDirInCreate = "NO_DIR_IN_CREATE"
|
||||
SQLModeNoEngineSubstitution = "NO_ENGINE_SUBSTITUTION"
|
||||
SQLModeNoFieldOptions = "NO_FIELD_OPTIONS"
|
||||
SQLModeNoKeyOptions = "NO_KEY_OPTIONS"
|
||||
SQLModeNoTableOptions = "NO_TABLE_OPTIONS"
|
||||
SQLModeNoUnsignedSubtraction = "NO_UNSIGNED_SUBTRACTION"
|
||||
SQLModeNoZeroDate = "NO_ZERO_DATE"
|
||||
SQLModeNoZeroInDate = "NO_ZERO_IN_DATE"
|
||||
SQLModeOnlyFullGroupBy = "ONLY_FULL_GROUP_BY"
|
||||
SQLModePadCharToFullLength = "PAD_CHAR_TO_FULL_LENGTH"
|
||||
SQLModePipesAsConcat = "PIPES_AS_CONCAT"
|
||||
SQLModeRealAsFloat = "REAL_AS_FLOAT"
|
||||
SQLModeStrictAllTables = "STRICT_ALL_TABLES"
|
||||
SQLModeStrictTransTables = "STRICT_TRANS_TABLES"
|
||||
SQLModeANSI = "ANSI"
|
||||
SQLModeDB2 = "DB2"
|
||||
SQLModeMaxDB = "MAXDB"
|
||||
SQLModeMSSQL = "MSSQL"
|
||||
SQLModeMYSQL323 = "MYSQL323"
|
||||
SQLModeMYSQL40 = "MYSQL40"
|
||||
SQLModeOracle = "ORACLE"
|
||||
SQLModePostgreSQL = "POSTGRESQL"
|
||||
SQLModeTraditional = "TRADITIONAL"
|
||||
)
|
||||
|
||||
// SQL Auth constants allow for MySQL-specific user auth plugins
|
||||
const (
|
||||
SQLAuthPluginNative = "mysql_native_password"
|
||||
SQLAuthPluginCachingSHA2 = "caching_sha2_password"
|
||||
)
|
||||
|
||||
// Redis eviction policies supported by the managed Redis product.
|
||||
const (
|
||||
EvictionPolicyNoEviction = "noeviction"
|
||||
EvictionPolicyAllKeysLRU = "allkeys_lru"
|
||||
EvictionPolicyAllKeysRandom = "allkeys_random"
|
||||
EvictionPolicyVolatileLRU = "volatile_lru"
|
||||
EvictionPolicyVolatileRandom = "volatile_random"
|
||||
EvictionPolicyVolatileTTL = "volatile_ttl"
|
||||
)
|
||||
|
||||
// The DatabasesService provides access to the DigitalOcean managed database
|
||||
// suite of products through the public API. Customers can create new database
|
||||
// clusters, migrate them between regions, create replicas and interact with
|
||||
// their configurations. Each database service is refered to as a Database. A
|
||||
// SQL database service can have multiple databases residing in the system. To
|
||||
// help make these entities distinct from Databases in godo, we refer to them
|
||||
// here as DatabaseDBs.
|
||||
//
|
||||
// See: https://developers.digitalocean.com/documentation/v2#databases
|
||||
type DatabasesService interface {
|
||||
List(context.Context, *ListOptions) ([]Database, *Response, error)
|
||||
Get(context.Context, string) (*Database, *Response, error)
|
||||
Create(context.Context, *DatabaseCreateRequest) (*Database, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
Resize(context.Context, string, *DatabaseResizeRequest) (*Response, error)
|
||||
Migrate(context.Context, string, *DatabaseMigrateRequest) (*Response, error)
|
||||
UpdateMaintenance(context.Context, string, *DatabaseUpdateMaintenanceRequest) (*Response, error)
|
||||
ListBackups(context.Context, string, *ListOptions) ([]DatabaseBackup, *Response, error)
|
||||
GetUser(context.Context, string, string) (*DatabaseUser, *Response, error)
|
||||
ListUsers(context.Context, string, *ListOptions) ([]DatabaseUser, *Response, error)
|
||||
CreateUser(context.Context, string, *DatabaseCreateUserRequest) (*DatabaseUser, *Response, error)
|
||||
DeleteUser(context.Context, string, string) (*Response, error)
|
||||
ResetUserAuth(context.Context, string, string, *DatabaseResetUserAuthRequest) (*DatabaseUser, *Response, error)
|
||||
ListDBs(context.Context, string, *ListOptions) ([]DatabaseDB, *Response, error)
|
||||
CreateDB(context.Context, string, *DatabaseCreateDBRequest) (*DatabaseDB, *Response, error)
|
||||
GetDB(context.Context, string, string) (*DatabaseDB, *Response, error)
|
||||
DeleteDB(context.Context, string, string) (*Response, error)
|
||||
ListPools(context.Context, string, *ListOptions) ([]DatabasePool, *Response, error)
|
||||
CreatePool(context.Context, string, *DatabaseCreatePoolRequest) (*DatabasePool, *Response, error)
|
||||
GetPool(context.Context, string, string) (*DatabasePool, *Response, error)
|
||||
DeletePool(context.Context, string, string) (*Response, error)
|
||||
GetReplica(context.Context, string, string) (*DatabaseReplica, *Response, error)
|
||||
ListReplicas(context.Context, string, *ListOptions) ([]DatabaseReplica, *Response, error)
|
||||
CreateReplica(context.Context, string, *DatabaseCreateReplicaRequest) (*DatabaseReplica, *Response, error)
|
||||
DeleteReplica(context.Context, string, string) (*Response, error)
|
||||
GetEvictionPolicy(context.Context, string) (string, *Response, error)
|
||||
SetEvictionPolicy(context.Context, string, string) (*Response, error)
|
||||
GetSQLMode(context.Context, string) (string, *Response, error)
|
||||
SetSQLMode(context.Context, string, ...string) (*Response, error)
|
||||
GetFirewallRules(context.Context, string) ([]DatabaseFirewallRule, *Response, error)
|
||||
UpdateFirewallRules(context.Context, string, *DatabaseUpdateFirewallRulesRequest) (*Response, error)
|
||||
}
|
||||
|
||||
// DatabasesServiceOp handles communication with the Databases related methods
|
||||
// of the DigitalOcean API.
|
||||
type DatabasesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ DatabasesService = &DatabasesServiceOp{}
|
||||
|
||||
// Database represents a DigitalOcean managed database product. These managed databases
|
||||
// are usually comprised of a cluster of database nodes, a primary and 0 or more replicas.
|
||||
// The EngineSlug is a string which indicates the type of database service. Some examples are
|
||||
// "pg", "mysql" or "redis". A Database also includes connection information and other
|
||||
// properties of the service like region, size and current status.
|
||||
type Database struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EngineSlug string `json:"engine,omitempty"`
|
||||
VersionSlug string `json:"version,omitempty"`
|
||||
Connection *DatabaseConnection `json:"connection,omitempty"`
|
||||
PrivateConnection *DatabaseConnection `json:"private_connection,omitempty"`
|
||||
Users []DatabaseUser `json:"users,omitempty"`
|
||||
NumNodes int `json:"num_nodes,omitempty"`
|
||||
SizeSlug string `json:"size,omitempty"`
|
||||
DBNames []string `json:"db_names,omitempty"`
|
||||
RegionSlug string `json:"region,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
MaintenanceWindow *DatabaseMaintenanceWindow `json:"maintenance_window,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
PrivateNetworkUUID string `json:"private_network_uuid,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseConnection represents a database connection
|
||||
type DatabaseConnection struct {
|
||||
URI string `json:"uri,omitempty"`
|
||||
Database string `json:"database,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
SSL bool `json:"ssl,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseUser represents a user in the database
|
||||
type DatabaseUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
MySQLSettings *DatabaseMySQLUserSettings `json:"mysql_settings,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseMySQLUserSettings contains MySQL-specific user settings
|
||||
type DatabaseMySQLUserSettings struct {
|
||||
AuthPlugin string `json:"auth_plugin"`
|
||||
}
|
||||
|
||||
// DatabaseMaintenanceWindow represents the maintenance_window of a database
|
||||
// cluster
|
||||
type DatabaseMaintenanceWindow struct {
|
||||
Day string `json:"day,omitempty"`
|
||||
Hour string `json:"hour,omitempty"`
|
||||
Pending bool `json:"pending,omitempty"`
|
||||
Description []string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseBackup represents a database backup.
|
||||
type DatabaseBackup struct {
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
SizeGigabytes float64 `json:"size_gigabytes,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseCreateRequest represents a request to create a database cluster
|
||||
type DatabaseCreateRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
EngineSlug string `json:"engine,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
SizeSlug string `json:"size,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
NumNodes int `json:"num_nodes,omitempty"`
|
||||
PrivateNetworkUUID string `json:"private_network_uuid"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseResizeRequest can be used to initiate a database resize operation.
|
||||
type DatabaseResizeRequest struct {
|
||||
SizeSlug string `json:"size,omitempty"`
|
||||
NumNodes int `json:"num_nodes,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseMigrateRequest can be used to initiate a database migrate operation.
|
||||
type DatabaseMigrateRequest struct {
|
||||
Region string `json:"region,omitempty"`
|
||||
PrivateNetworkUUID string `json:"private_network_uuid"`
|
||||
}
|
||||
|
||||
// DatabaseUpdateMaintenanceRequest can be used to update the database's maintenance window.
|
||||
type DatabaseUpdateMaintenanceRequest struct {
|
||||
Day string `json:"day,omitempty"`
|
||||
Hour string `json:"hour,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseDB represents an engine-specific database created within a database cluster. For SQL
|
||||
// databases like PostgreSQL or MySQL, a "DB" refers to a database created on the RDBMS. For instance,
|
||||
// a PostgreSQL database server can contain many database schemas, each with it's own settings, access
|
||||
// permissions and data. ListDBs will return all databases present on the server.
|
||||
type DatabaseDB struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// DatabaseReplica represents a read-only replica of a particular database
|
||||
type DatabaseReplica struct {
|
||||
Name string `json:"name"`
|
||||
Connection *DatabaseConnection `json:"connection"`
|
||||
PrivateConnection *DatabaseConnection `json:"private_connection,omitempty"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PrivateNetworkUUID string `json:"private_network_uuid,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// DatabasePool represents a database connection pool
|
||||
type DatabasePool struct {
|
||||
User string `json:"user"`
|
||||
Name string `json:"name"`
|
||||
Size int `json:"size"`
|
||||
Database string `json:"db"`
|
||||
Mode string `json:"mode"`
|
||||
Connection *DatabaseConnection `json:"connection"`
|
||||
PrivateConnection *DatabaseConnection `json:"private_connection,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseCreatePoolRequest is used to create a new database connection pool
|
||||
type DatabaseCreatePoolRequest struct {
|
||||
User string `json:"user"`
|
||||
Name string `json:"name"`
|
||||
Size int `json:"size"`
|
||||
Database string `json:"db"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
// DatabaseCreateUserRequest is used to create a new database user
|
||||
type DatabaseCreateUserRequest struct {
|
||||
Name string `json:"name"`
|
||||
MySQLSettings *DatabaseMySQLUserSettings `json:"mysql_settings,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseResetUserAuth request is used to reset a users DB auth
|
||||
type DatabaseResetUserAuthRequest struct {
|
||||
MySQLSettings *DatabaseMySQLUserSettings `json:"mysql_settings,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseCreateDBRequest is used to create a new engine-specific database within the cluster
|
||||
type DatabaseCreateDBRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// DatabaseCreateReplicaRequest is used to create a new read-only replica
|
||||
type DatabaseCreateReplicaRequest struct {
|
||||
Name string `json:"name"`
|
||||
Region string `json:"region"`
|
||||
Size string `json:"size"`
|
||||
PrivateNetworkUUID string `json:"private_network_uuid"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseUpdateFirewallRulesRequest is used to set the firewall rules for a database
|
||||
type DatabaseUpdateFirewallRulesRequest struct {
|
||||
Rules []*DatabaseFirewallRule `json:"rules"`
|
||||
}
|
||||
|
||||
// DatabaseFirewallRule is a rule describing an inbound source to a database
|
||||
type DatabaseFirewallRule struct {
|
||||
UUID string `json:"uuid"`
|
||||
ClusterUUID string `json:"cluster_uuid"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type databaseUserRoot struct {
|
||||
User *DatabaseUser `json:"user"`
|
||||
}
|
||||
|
||||
type databaseUsersRoot struct {
|
||||
Users []DatabaseUser `json:"users"`
|
||||
}
|
||||
|
||||
type databaseDBRoot struct {
|
||||
DB *DatabaseDB `json:"db"`
|
||||
}
|
||||
|
||||
type databaseDBsRoot struct {
|
||||
DBs []DatabaseDB `json:"dbs"`
|
||||
}
|
||||
|
||||
type databasesRoot struct {
|
||||
Databases []Database `json:"databases"`
|
||||
}
|
||||
|
||||
type databaseRoot struct {
|
||||
Database *Database `json:"database"`
|
||||
}
|
||||
|
||||
type databaseBackupsRoot struct {
|
||||
Backups []DatabaseBackup `json:"backups"`
|
||||
}
|
||||
|
||||
type databasePoolRoot struct {
|
||||
Pool *DatabasePool `json:"pool"`
|
||||
}
|
||||
|
||||
type databasePoolsRoot struct {
|
||||
Pools []DatabasePool `json:"pools"`
|
||||
}
|
||||
|
||||
type databaseReplicaRoot struct {
|
||||
Replica *DatabaseReplica `json:"replica"`
|
||||
}
|
||||
|
||||
type databaseReplicasRoot struct {
|
||||
Replicas []DatabaseReplica `json:"replicas"`
|
||||
}
|
||||
|
||||
type evictionPolicyRoot struct {
|
||||
EvictionPolicy string `json:"eviction_policy"`
|
||||
}
|
||||
|
||||
type sqlModeRoot struct {
|
||||
SQLMode string `json:"sql_mode"`
|
||||
}
|
||||
|
||||
type databaseFirewallRuleRoot struct {
|
||||
Rules []DatabaseFirewallRule `json:"rules"`
|
||||
}
|
||||
|
||||
// URN returns a URN identifier for the database
|
||||
func (d Database) URN() string {
|
||||
return ToURN("dbaas", d.ID)
|
||||
}
|
||||
|
||||
// List returns a list of the Databases visible with the caller's API token
|
||||
func (svc *DatabasesServiceOp) List(ctx context.Context, opts *ListOptions) ([]Database, *Response, error) {
|
||||
path := databaseBasePath
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databasesRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Databases, resp, nil
|
||||
}
|
||||
|
||||
// Get retrieves the details of a database cluster
|
||||
func (svc *DatabasesServiceOp) Get(ctx context.Context, databaseID string) (*Database, *Response, error) {
|
||||
path := fmt.Sprintf(databaseSinglePath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Database, resp, nil
|
||||
}
|
||||
|
||||
// Create creates a database cluster
|
||||
func (svc *DatabasesServiceOp) Create(ctx context.Context, create *DatabaseCreateRequest) (*Database, *Response, error) {
|
||||
path := databaseBasePath
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Database, resp, nil
|
||||
}
|
||||
|
||||
// Delete deletes a database cluster. There is no way to recover a cluster once
|
||||
// it has been destroyed.
|
||||
func (svc *DatabasesServiceOp) Delete(ctx context.Context, databaseID string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", databaseBasePath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Resize resizes a database cluster by number of nodes or size
|
||||
func (svc *DatabasesServiceOp) Resize(ctx context.Context, databaseID string, resize *DatabaseResizeRequest) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseResizePath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPut, path, resize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Migrate migrates a database cluster to a new region
|
||||
func (svc *DatabasesServiceOp) Migrate(ctx context.Context, databaseID string, migrate *DatabaseMigrateRequest) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseMigratePath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPut, path, migrate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UpdateMaintenance updates the maintenance window on a cluster
|
||||
func (svc *DatabasesServiceOp) UpdateMaintenance(ctx context.Context, databaseID string, maintenance *DatabaseUpdateMaintenanceRequest) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseMaintenancePath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPut, path, maintenance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListBackups returns a list of the current backups of a database
|
||||
func (svc *DatabasesServiceOp) ListBackups(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseBackup, *Response, error) {
|
||||
path := fmt.Sprintf(databaseBackupsPath, databaseID)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseBackupsRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Backups, resp, nil
|
||||
}
|
||||
|
||||
// GetUser returns the database user identified by userID
|
||||
func (svc *DatabasesServiceOp) GetUser(ctx context.Context, databaseID, userID string) (*DatabaseUser, *Response, error) {
|
||||
path := fmt.Sprintf(databaseUserPath, databaseID, userID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseUserRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.User, resp, nil
|
||||
}
|
||||
|
||||
// ListUsers returns all database users for the database
|
||||
func (svc *DatabasesServiceOp) ListUsers(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseUser, *Response, error) {
|
||||
path := fmt.Sprintf(databaseUsersPath, databaseID)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseUsersRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Users, resp, nil
|
||||
}
|
||||
|
||||
// CreateUser will create a new database user
|
||||
func (svc *DatabasesServiceOp) CreateUser(ctx context.Context, databaseID string, createUser *DatabaseCreateUserRequest) (*DatabaseUser, *Response, error) {
|
||||
path := fmt.Sprintf(databaseUsersPath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createUser)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseUserRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.User, resp, nil
|
||||
}
|
||||
|
||||
func (svc *DatabasesServiceOp) ResetUserAuth(ctx context.Context, databaseID, userID string, resetAuth *DatabaseResetUserAuthRequest) (*DatabaseUser, *Response, error) {
|
||||
path := fmt.Sprintf(databaseResetUserAuthPath, databaseID, userID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, resetAuth)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseUserRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.User, resp, nil
|
||||
}
|
||||
|
||||
// DeleteUser will delete an existing database user
|
||||
func (svc *DatabasesServiceOp) DeleteUser(ctx context.Context, databaseID, userID string) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseUserPath, databaseID, userID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListDBs returns all databases for a given database cluster
|
||||
func (svc *DatabasesServiceOp) ListDBs(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseDB, *Response, error) {
|
||||
path := fmt.Sprintf(databaseDBsPath, databaseID)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseDBsRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.DBs, resp, nil
|
||||
}
|
||||
|
||||
// GetDB returns a single database by name
|
||||
func (svc *DatabasesServiceOp) GetDB(ctx context.Context, databaseID, name string) (*DatabaseDB, *Response, error) {
|
||||
path := fmt.Sprintf(databaseDBPath, databaseID, name)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseDBRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.DB, resp, nil
|
||||
}
|
||||
|
||||
// CreateDB will create a new database
|
||||
func (svc *DatabasesServiceOp) CreateDB(ctx context.Context, databaseID string, createDB *DatabaseCreateDBRequest) (*DatabaseDB, *Response, error) {
|
||||
path := fmt.Sprintf(databaseDBsPath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createDB)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseDBRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.DB, resp, nil
|
||||
}
|
||||
|
||||
// DeleteDB will delete an existing database
|
||||
func (svc *DatabasesServiceOp) DeleteDB(ctx context.Context, databaseID, name string) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseDBPath, databaseID, name)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListPools returns all connection pools for a given database cluster
|
||||
func (svc *DatabasesServiceOp) ListPools(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabasePool, *Response, error) {
|
||||
path := fmt.Sprintf(databasePoolsPath, databaseID)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databasePoolsRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Pools, resp, nil
|
||||
}
|
||||
|
||||
// GetPool returns a single database connection pool by name
|
||||
func (svc *DatabasesServiceOp) GetPool(ctx context.Context, databaseID, name string) (*DatabasePool, *Response, error) {
|
||||
path := fmt.Sprintf(databasePoolPath, databaseID, name)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databasePoolRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Pool, resp, nil
|
||||
}
|
||||
|
||||
// CreatePool will create a new database connection pool
|
||||
func (svc *DatabasesServiceOp) CreatePool(ctx context.Context, databaseID string, createPool *DatabaseCreatePoolRequest) (*DatabasePool, *Response, error) {
|
||||
path := fmt.Sprintf(databasePoolsPath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createPool)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databasePoolRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Pool, resp, nil
|
||||
}
|
||||
|
||||
// DeletePool will delete an existing database connection pool
|
||||
func (svc *DatabasesServiceOp) DeletePool(ctx context.Context, databaseID, name string) (*Response, error) {
|
||||
path := fmt.Sprintf(databasePoolPath, databaseID, name)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetReplica returns a single database replica
|
||||
func (svc *DatabasesServiceOp) GetReplica(ctx context.Context, databaseID, name string) (*DatabaseReplica, *Response, error) {
|
||||
path := fmt.Sprintf(databaseReplicaPath, databaseID, name)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseReplicaRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Replica, resp, nil
|
||||
}
|
||||
|
||||
// ListReplicas returns all read-only replicas for a given database cluster
|
||||
func (svc *DatabasesServiceOp) ListReplicas(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseReplica, *Response, error) {
|
||||
path := fmt.Sprintf(databaseReplicasPath, databaseID)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseReplicasRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Replicas, resp, nil
|
||||
}
|
||||
|
||||
// CreateReplica will create a new database connection pool
|
||||
func (svc *DatabasesServiceOp) CreateReplica(ctx context.Context, databaseID string, createReplica *DatabaseCreateReplicaRequest) (*DatabaseReplica, *Response, error) {
|
||||
path := fmt.Sprintf(databaseReplicasPath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createReplica)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(databaseReplicaRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Replica, resp, nil
|
||||
}
|
||||
|
||||
// DeleteReplica will delete an existing database replica
|
||||
func (svc *DatabasesServiceOp) DeleteReplica(ctx context.Context, databaseID, name string) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseReplicaPath, databaseID, name)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetEvictionPolicy loads the eviction policy for a given Redis cluster.
|
||||
func (svc *DatabasesServiceOp) GetEvictionPolicy(ctx context.Context, databaseID string) (string, *Response, error) {
|
||||
path := fmt.Sprintf(databaseEvictionPolicyPath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
root := new(evictionPolicyRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
return root.EvictionPolicy, resp, nil
|
||||
}
|
||||
|
||||
// SetEvictionPolicy updates the eviction policy for a given Redis cluster.
|
||||
//
|
||||
// The valid eviction policies are documented by the exported string constants
|
||||
// with the prefix `EvictionPolicy`.
|
||||
func (svc *DatabasesServiceOp) SetEvictionPolicy(ctx context.Context, databaseID, policy string) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseEvictionPolicyPath, databaseID)
|
||||
root := &evictionPolicyRoot{EvictionPolicy: policy}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPut, path, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetSQLMode loads the SQL Mode settings for a given MySQL cluster.
|
||||
func (svc *DatabasesServiceOp) GetSQLMode(ctx context.Context, databaseID string) (string, *Response, error) {
|
||||
path := fmt.Sprintf(databaseSQLModePath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
root := &sqlModeRoot{}
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
return root.SQLMode, resp, nil
|
||||
}
|
||||
|
||||
// SetSQLMode updates the SQL Mode settings for a given MySQL cluster.
|
||||
func (svc *DatabasesServiceOp) SetSQLMode(ctx context.Context, databaseID string, sqlModes ...string) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseSQLModePath, databaseID)
|
||||
root := &sqlModeRoot{SQLMode: strings.Join(sqlModes, ",")}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPut, path, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetFirewallRules loads the inbound sources for a given cluster.
|
||||
func (svc *DatabasesServiceOp) GetFirewallRules(ctx context.Context, databaseID string) ([]DatabaseFirewallRule, *Response, error) {
|
||||
path := fmt.Sprintf(databaseFirewallRulesPath, databaseID)
|
||||
root := new(databaseFirewallRuleRoot)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Rules, resp, nil
|
||||
}
|
||||
|
||||
// UpdateFirewallRules sets the inbound sources for a given cluster.
|
||||
func (svc *DatabasesServiceOp) UpdateFirewallRules(ctx context.Context, databaseID string, firewallRulesReq *DatabaseUpdateFirewallRulesRequest) (*Response, error) {
|
||||
path := fmt.Sprintf(databaseFirewallRulesPath, databaseID)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPut, path, firewallRulesReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.client.Do(ctx, req, nil)
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
// Package godo is the DigtalOcean API v2 client for Go.
|
||||
package godo
|
@ -0,0 +1,341 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const domainsBasePath = "v2/domains"
|
||||
|
||||
// DomainsService is an interface for managing DNS with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#domains and
|
||||
// https://developers.digitalocean.com/documentation/v2#domain-records
|
||||
type DomainsService interface {
|
||||
List(context.Context, *ListOptions) ([]Domain, *Response, error)
|
||||
Get(context.Context, string) (*Domain, *Response, error)
|
||||
Create(context.Context, *DomainCreateRequest) (*Domain, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
|
||||
Records(context.Context, string, *ListOptions) ([]DomainRecord, *Response, error)
|
||||
Record(context.Context, string, int) (*DomainRecord, *Response, error)
|
||||
DeleteRecord(context.Context, string, int) (*Response, error)
|
||||
EditRecord(context.Context, string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
|
||||
CreateRecord(context.Context, string, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
|
||||
}
|
||||
|
||||
// DomainsServiceOp handles communication with the domain related methods of the
|
||||
// DigitalOcean API.
|
||||
type DomainsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ DomainsService = &DomainsServiceOp{}
|
||||
|
||||
// Domain represents a DigitalOcean domain
|
||||
type Domain struct {
|
||||
Name string `json:"name"`
|
||||
TTL int `json:"ttl"`
|
||||
ZoneFile string `json:"zone_file"`
|
||||
}
|
||||
|
||||
// domainRoot represents a response from the DigitalOcean API
|
||||
type domainRoot struct {
|
||||
Domain *Domain `json:"domain"`
|
||||
}
|
||||
|
||||
type domainsRoot struct {
|
||||
Domains []Domain `json:"domains"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// DomainCreateRequest respresents a request to create a domain.
|
||||
type DomainCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
}
|
||||
|
||||
// DomainRecordRoot is the root of an individual Domain Record response
|
||||
type domainRecordRoot struct {
|
||||
DomainRecord *DomainRecord `json:"domain_record"`
|
||||
}
|
||||
|
||||
// DomainRecordsRoot is the root of a group of Domain Record responses
|
||||
type domainRecordsRoot struct {
|
||||
DomainRecords []DomainRecord `json:"domain_records"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// DomainRecord represents a DigitalOcean DomainRecord
|
||||
type DomainRecord struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
Port int `json:"port,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
Weight int `json:"weight"`
|
||||
Flags int `json:"flags"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
// DomainRecordEditRequest represents a request to update a domain record.
|
||||
type DomainRecordEditRequest struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
Port int `json:"port,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
Weight int `json:"weight"`
|
||||
Flags int `json:"flags"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
func (d Domain) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
func (d Domain) URN() string {
|
||||
return ToURN("Domain", d.Name)
|
||||
}
|
||||
|
||||
// List all domains.
|
||||
func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) {
|
||||
path := domainsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Domains, resp, err
|
||||
}
|
||||
|
||||
// Get individual domain. It requires a non-empty domain name.
|
||||
func (s *DomainsServiceOp) Get(ctx context.Context, name string) (*Domain, *Response, error) {
|
||||
if len(name) < 1 {
|
||||
return nil, nil, NewArgError("name", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Domain, resp, err
|
||||
}
|
||||
|
||||
// Create a new domain
|
||||
func (s *DomainsServiceOp) Create(ctx context.Context, createRequest *DomainCreateRequest) (*Domain, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := domainsBasePath
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Domain, resp, err
|
||||
}
|
||||
|
||||
// Delete domain
|
||||
func (s *DomainsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
|
||||
if len(name) < 1 {
|
||||
return nil, NewArgError("name", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Converts a DomainRecord to a string.
|
||||
func (d DomainRecord) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Converts a DomainRecordEditRequest to a string.
|
||||
func (d DomainRecordEditRequest) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Records returns a slice of DomainRecords for a domain
|
||||
func (s *DomainsServiceOp) Records(ctx context.Context, domain string, opt *ListOptions) ([]DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainRecordsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.DomainRecords, resp, err
|
||||
}
|
||||
|
||||
// Record returns the record id from a domain
|
||||
func (s *DomainsServiceOp) Record(ctx context.Context, domain string, id int) (*DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
record := new(domainRecordRoot)
|
||||
resp, err := s.client.Do(ctx, req, record)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return record.DomainRecord, resp, err
|
||||
}
|
||||
|
||||
// DeleteRecord deletes a record from a domain identified by id
|
||||
func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id int) (*Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
if id < 1 {
|
||||
return nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// EditRecord edits a record using a DomainRecordEditRequest
|
||||
func (s *DomainsServiceOp) EditRecord(ctx context.Context,
|
||||
domain string,
|
||||
id int,
|
||||
editRequest *DomainRecordEditRequest,
|
||||
) (*DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if editRequest == nil {
|
||||
return nil, nil, NewArgError("editRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPut, path, editRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainRecordRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.DomainRecord, resp, err
|
||||
}
|
||||
|
||||
// CreateRecord creates a record using a DomainRecordEditRequest
|
||||
func (s *DomainsServiceOp) CreateRecord(ctx context.Context,
|
||||
domain string,
|
||||
createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be empty string")
|
||||
}
|
||||
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
d := new(domainRecordRoot)
|
||||
resp, err := s.client.Do(ctx, req, d)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return d.DomainRecord, resp, err
|
||||
}
|
@ -0,0 +1,329 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ActionRequest reprents DigitalOcean Action Request
|
||||
type ActionRequest map[string]interface{}
|
||||
|
||||
// DropletActionsService is an interface for interfacing with the Droplet actions
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#droplet-actions
|
||||
type DropletActionsService interface {
|
||||
Shutdown(context.Context, int) (*Action, *Response, error)
|
||||
ShutdownByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PowerOff(context.Context, int) (*Action, *Response, error)
|
||||
PowerOffByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PowerOn(context.Context, int) (*Action, *Response, error)
|
||||
PowerOnByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PowerCycle(context.Context, int) (*Action, *Response, error)
|
||||
PowerCycleByTag(context.Context, string) ([]Action, *Response, error)
|
||||
Reboot(context.Context, int) (*Action, *Response, error)
|
||||
Restore(context.Context, int, int) (*Action, *Response, error)
|
||||
Resize(context.Context, int, string, bool) (*Action, *Response, error)
|
||||
Rename(context.Context, int, string) (*Action, *Response, error)
|
||||
Snapshot(context.Context, int, string) (*Action, *Response, error)
|
||||
SnapshotByTag(context.Context, string, string) ([]Action, *Response, error)
|
||||
EnableBackups(context.Context, int) (*Action, *Response, error)
|
||||
EnableBackupsByTag(context.Context, string) ([]Action, *Response, error)
|
||||
DisableBackups(context.Context, int) (*Action, *Response, error)
|
||||
DisableBackupsByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PasswordReset(context.Context, int) (*Action, *Response, error)
|
||||
RebuildByImageID(context.Context, int, int) (*Action, *Response, error)
|
||||
RebuildByImageSlug(context.Context, int, string) (*Action, *Response, error)
|
||||
ChangeKernel(context.Context, int, int) (*Action, *Response, error)
|
||||
EnableIPv6(context.Context, int) (*Action, *Response, error)
|
||||
EnableIPv6ByTag(context.Context, string) ([]Action, *Response, error)
|
||||
EnablePrivateNetworking(context.Context, int) (*Action, *Response, error)
|
||||
EnablePrivateNetworkingByTag(context.Context, string) ([]Action, *Response, error)
|
||||
Get(context.Context, int, int) (*Action, *Response, error)
|
||||
GetByURI(context.Context, string) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// DropletActionsServiceOp handles communication with the Droplet action related
|
||||
// methods of the DigitalOcean API.
|
||||
type DropletActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ DropletActionsService = &DropletActionsServiceOp{}
|
||||
|
||||
// Shutdown a Droplet
|
||||
func (s *DropletActionsServiceOp) Shutdown(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "shutdown"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// ShutdownByTag shuts down Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) ShutdownByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "shutdown"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PowerOff a Droplet
|
||||
func (s *DropletActionsServiceOp) PowerOff(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_off"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// PowerOffByTag powers off Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) PowerOffByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_off"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PowerOn a Droplet
|
||||
func (s *DropletActionsServiceOp) PowerOn(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_on"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// PowerOnByTag powers on Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) PowerOnByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_on"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PowerCycle a Droplet
|
||||
func (s *DropletActionsServiceOp) PowerCycle(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_cycle"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// PowerCycleByTag power cycles Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) PowerCycleByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_cycle"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// Reboot a Droplet
|
||||
func (s *DropletActionsServiceOp) Reboot(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "reboot"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Restore an image to a Droplet
|
||||
func (s *DropletActionsServiceOp) Restore(ctx context.Context, id, imageID int) (*Action, *Response, error) {
|
||||
requestType := "restore"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"image": float64(imageID),
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Resize a Droplet
|
||||
func (s *DropletActionsServiceOp) Resize(ctx context.Context, id int, sizeSlug string, resizeDisk bool) (*Action, *Response, error) {
|
||||
requestType := "resize"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"size": sizeSlug,
|
||||
"disk": resizeDisk,
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Rename a Droplet
|
||||
func (s *DropletActionsServiceOp) Rename(ctx context.Context, id int, name string) (*Action, *Response, error) {
|
||||
requestType := "rename"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"name": name,
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Snapshot a Droplet.
|
||||
func (s *DropletActionsServiceOp) Snapshot(ctx context.Context, id int, name string) (*Action, *Response, error) {
|
||||
requestType := "snapshot"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"name": name,
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// SnapshotByTag snapshots Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) SnapshotByTag(ctx context.Context, tag string, name string) ([]Action, *Response, error) {
|
||||
requestType := "snapshot"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"name": name,
|
||||
}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// EnableBackups enables backups for a Droplet.
|
||||
func (s *DropletActionsServiceOp) EnableBackups(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_backups"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnableBackupsByTag enables backups for Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_backups"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// DisableBackups disables backups for a Droplet.
|
||||
func (s *DropletActionsServiceOp) DisableBackups(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "disable_backups"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// DisableBackupsByTag disables backups for Droplet matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) DisableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "disable_backups"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PasswordReset resets the password for a Droplet.
|
||||
func (s *DropletActionsServiceOp) PasswordReset(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "password_reset"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// RebuildByImageID rebuilds a Droplet from an image with a given id.
|
||||
func (s *DropletActionsServiceOp) RebuildByImageID(ctx context.Context, id, imageID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "rebuild", "image": imageID}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// RebuildByImageSlug rebuilds a Droplet from an Image matched by a given Slug.
|
||||
func (s *DropletActionsServiceOp) RebuildByImageSlug(ctx context.Context, id int, slug string) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "rebuild", "image": slug}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// ChangeKernel changes the kernel for a Droplet.
|
||||
func (s *DropletActionsServiceOp) ChangeKernel(ctx context.Context, id, kernelID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "change_kernel", "kernel": kernelID}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnableIPv6 enables IPv6 for a Droplet.
|
||||
func (s *DropletActionsServiceOp) EnableIPv6(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_ipv6"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnableIPv6ByTag enables IPv6 for Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) EnableIPv6ByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_ipv6"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// EnablePrivateNetworking enables private networking for a Droplet.
|
||||
func (s *DropletActionsServiceOp) EnablePrivateNetworking(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_private_networking"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnablePrivateNetworkingByTag enables private networking for Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_private_networking"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request *ActionRequest) (*Action, *Response, error) {
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if request == nil {
|
||||
return nil, nil, NewArgError("request", "request can't be nil")
|
||||
}
|
||||
|
||||
path := dropletActionPath(id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) ([]Action, *Response, error) {
|
||||
if tag == "" {
|
||||
return nil, nil, NewArgError("tag", "cannot be empty")
|
||||
}
|
||||
|
||||
if request == nil {
|
||||
return nil, nil, NewArgError("request", "request can't be nil")
|
||||
}
|
||||
|
||||
path := dropletActionPathByTag(tag)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
// Get an action for a particular Droplet by id.
|
||||
func (s *DropletActionsServiceOp) Get(ctx context.Context, dropletID, actionID int) (*Action, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if actionID < 1 {
|
||||
return nil, nil, NewArgError("actionID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", dropletActionPath(dropletID), actionID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// GetByURI gets an action for a particular Droplet by id.
|
||||
func (s *DropletActionsServiceOp) GetByURI(ctx context.Context, rawurl string) (*Action, *Response, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.get(ctx, u.Path)
|
||||
|
||||
}
|
||||
|
||||
func (s *DropletActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
|
||||
}
|
||||
|
||||
func dropletActionPath(dropletID int) string {
|
||||
return fmt.Sprintf("v2/droplets/%d/actions", dropletID)
|
||||
}
|
||||
|
||||
func dropletActionPathByTag(tag string) string {
|
||||
return fmt.Sprintf("v2/droplets/actions?tag_name=%s", tag)
|
||||
}
|
@ -0,0 +1,592 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const dropletBasePath = "v2/droplets"
|
||||
|
||||
var errNoNetworks = errors.New("no networks have been defined")
|
||||
|
||||
// DropletsService is an interface for interfacing with the Droplet
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#droplets
|
||||
type DropletsService interface {
|
||||
List(context.Context, *ListOptions) ([]Droplet, *Response, error)
|
||||
ListByTag(context.Context, string, *ListOptions) ([]Droplet, *Response, error)
|
||||
Get(context.Context, int) (*Droplet, *Response, error)
|
||||
Create(context.Context, *DropletCreateRequest) (*Droplet, *Response, error)
|
||||
CreateMultiple(context.Context, *DropletMultiCreateRequest) ([]Droplet, *Response, error)
|
||||
Delete(context.Context, int) (*Response, error)
|
||||
DeleteByTag(context.Context, string) (*Response, error)
|
||||
Kernels(context.Context, int, *ListOptions) ([]Kernel, *Response, error)
|
||||
Snapshots(context.Context, int, *ListOptions) ([]Image, *Response, error)
|
||||
Backups(context.Context, int, *ListOptions) ([]Image, *Response, error)
|
||||
Actions(context.Context, int, *ListOptions) ([]Action, *Response, error)
|
||||
Neighbors(context.Context, int) ([]Droplet, *Response, error)
|
||||
}
|
||||
|
||||
// DropletsServiceOp handles communication with the Droplet related methods of the
|
||||
// DigitalOcean API.
|
||||
type DropletsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ DropletsService = &DropletsServiceOp{}
|
||||
|
||||
// Droplet represents a DigitalOcean Droplet
|
||||
type Droplet struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Memory int `json:"memory,omitempty"`
|
||||
Vcpus int `json:"vcpus,omitempty"`
|
||||
Disk int `json:"disk,omitempty"`
|
||||
Region *Region `json:"region,omitempty"`
|
||||
Image *Image `json:"image,omitempty"`
|
||||
Size *Size `json:"size,omitempty"`
|
||||
SizeSlug string `json:"size_slug,omitempty"`
|
||||
BackupIDs []int `json:"backup_ids,omitempty"`
|
||||
NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"`
|
||||
SnapshotIDs []int `json:"snapshot_ids,omitempty"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
Locked bool `json:"locked,bool,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Networks *Networks `json:"networks,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
Kernel *Kernel `json:"kernel,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
VolumeIDs []string `json:"volume_ids"`
|
||||
VPCUUID string `json:"vpc_uuid,omitempty"`
|
||||
}
|
||||
|
||||
// PublicIPv4 returns the public IPv4 address for the Droplet.
|
||||
func (d *Droplet) PublicIPv4() (string, error) {
|
||||
if d.Networks == nil {
|
||||
return "", errNoNetworks
|
||||
}
|
||||
|
||||
for _, v4 := range d.Networks.V4 {
|
||||
if v4.Type == "public" {
|
||||
return v4.IPAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// PrivateIPv4 returns the private IPv4 address for the Droplet.
|
||||
func (d *Droplet) PrivateIPv4() (string, error) {
|
||||
if d.Networks == nil {
|
||||
return "", errNoNetworks
|
||||
}
|
||||
|
||||
for _, v4 := range d.Networks.V4 {
|
||||
if v4.Type == "private" {
|
||||
return v4.IPAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// PublicIPv6 returns the public IPv6 address for the Droplet.
|
||||
func (d *Droplet) PublicIPv6() (string, error) {
|
||||
if d.Networks == nil {
|
||||
return "", errNoNetworks
|
||||
}
|
||||
|
||||
for _, v6 := range d.Networks.V6 {
|
||||
if v6.Type == "public" {
|
||||
return v6.IPAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Kernel object
|
||||
type Kernel struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// BackupWindow object
|
||||
type BackupWindow struct {
|
||||
Start *Timestamp `json:"start,omitempty"`
|
||||
End *Timestamp `json:"end,omitempty"`
|
||||
}
|
||||
|
||||
// Convert Droplet to a string
|
||||
func (d Droplet) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
func (d Droplet) URN() string {
|
||||
return ToURN("Droplet", d.ID)
|
||||
}
|
||||
|
||||
// DropletRoot represents a Droplet root
|
||||
type dropletRoot struct {
|
||||
Droplet *Droplet `json:"droplet"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
type dropletsRoot struct {
|
||||
Droplets []Droplet `json:"droplets"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type kernelsRoot struct {
|
||||
Kernels []Kernel `json:"kernels,omitempty"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type dropletSnapshotsRoot struct {
|
||||
Snapshots []Image `json:"snapshots,omitempty"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type backupsRoot struct {
|
||||
Backups []Image `json:"backups,omitempty"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// DropletCreateImage identifies an image for the create request. It prefers slug over ID.
|
||||
type DropletCreateImage struct {
|
||||
ID int
|
||||
Slug string
|
||||
}
|
||||
|
||||
// MarshalJSON returns either the slug or id of the image. It returns the id
|
||||
// if the slug is empty.
|
||||
func (d DropletCreateImage) MarshalJSON() ([]byte, error) {
|
||||
if d.Slug != "" {
|
||||
return json.Marshal(d.Slug)
|
||||
}
|
||||
|
||||
return json.Marshal(d.ID)
|
||||
}
|
||||
|
||||
// DropletCreateVolume identifies a volume to attach for the create request. It
|
||||
// prefers Name over ID,
|
||||
type DropletCreateVolume struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// MarshalJSON returns an object with either the name or id of the volume. It
|
||||
// returns the id if the name is empty.
|
||||
func (d DropletCreateVolume) MarshalJSON() ([]byte, error) {
|
||||
if d.Name != "" {
|
||||
return json.Marshal(struct {
|
||||
Name string `json:"name"`
|
||||
}{Name: d.Name})
|
||||
}
|
||||
|
||||
return json.Marshal(struct {
|
||||
ID string `json:"id"`
|
||||
}{ID: d.ID})
|
||||
}
|
||||
|
||||
// DropletCreateSSHKey identifies a SSH Key for the create request. It prefers fingerprint over ID.
|
||||
type DropletCreateSSHKey struct {
|
||||
ID int
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// MarshalJSON returns either the fingerprint or id of the ssh key. It returns
|
||||
// the id if the fingerprint is empty.
|
||||
func (d DropletCreateSSHKey) MarshalJSON() ([]byte, error) {
|
||||
if d.Fingerprint != "" {
|
||||
return json.Marshal(d.Fingerprint)
|
||||
}
|
||||
|
||||
return json.Marshal(d.ID)
|
||||
}
|
||||
|
||||
// DropletCreateRequest represents a request to create a Droplet.
|
||||
type DropletCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Region string `json:"region"`
|
||||
Size string `json:"size"`
|
||||
Image DropletCreateImage `json:"image"`
|
||||
SSHKeys []DropletCreateSSHKey `json:"ssh_keys"`
|
||||
Backups bool `json:"backups"`
|
||||
IPv6 bool `json:"ipv6"`
|
||||
PrivateNetworking bool `json:"private_networking"`
|
||||
Monitoring bool `json:"monitoring"`
|
||||
UserData string `json:"user_data,omitempty"`
|
||||
Volumes []DropletCreateVolume `json:"volumes,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
VPCUUID string `json:"vpc_uuid,omitempty"`
|
||||
}
|
||||
|
||||
// DropletMultiCreateRequest is a request to create multiple Droplets.
|
||||
type DropletMultiCreateRequest struct {
|
||||
Names []string `json:"names"`
|
||||
Region string `json:"region"`
|
||||
Size string `json:"size"`
|
||||
Image DropletCreateImage `json:"image"`
|
||||
SSHKeys []DropletCreateSSHKey `json:"ssh_keys"`
|
||||
Backups bool `json:"backups"`
|
||||
IPv6 bool `json:"ipv6"`
|
||||
PrivateNetworking bool `json:"private_networking"`
|
||||
Monitoring bool `json:"monitoring"`
|
||||
UserData string `json:"user_data,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
VPCUUID string `json:"vpc_uuid,omitempty"`
|
||||
}
|
||||
|
||||
func (d DropletCreateRequest) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
func (d DropletMultiCreateRequest) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Networks represents the Droplet's Networks.
|
||||
type Networks struct {
|
||||
V4 []NetworkV4 `json:"v4,omitempty"`
|
||||
V6 []NetworkV6 `json:"v6,omitempty"`
|
||||
}
|
||||
|
||||
// NetworkV4 represents a DigitalOcean IPv4 Network.
|
||||
type NetworkV4 struct {
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
Netmask string `json:"netmask,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (n NetworkV4) String() string {
|
||||
return Stringify(n)
|
||||
}
|
||||
|
||||
// NetworkV6 represents a DigitalOcean IPv6 network.
|
||||
type NetworkV6 struct {
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
Netmask int `json:"netmask,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (n NetworkV6) String() string {
|
||||
return Stringify(n)
|
||||
}
|
||||
|
||||
// Performs a list request given a path.
|
||||
func (s *DropletsServiceOp) list(ctx context.Context, path string) ([]Droplet, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Droplets, resp, err
|
||||
}
|
||||
|
||||
// List all Droplets.
|
||||
func (s *DropletsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Droplet, *Response, error) {
|
||||
path := dropletBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
// ListByTag lists all Droplets matched by a Tag.
|
||||
func (s *DropletsServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Droplet, *Response, error) {
|
||||
path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
// Get individual Droplet.
|
||||
func (s *DropletsServiceOp) Get(ctx context.Context, dropletID int) (*Droplet, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Droplet, resp, err
|
||||
}
|
||||
|
||||
// Create Droplet
|
||||
func (s *DropletsServiceOp) Create(ctx context.Context, createRequest *DropletCreateRequest) (*Droplet, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := dropletBasePath
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Droplet, resp, err
|
||||
}
|
||||
|
||||
// CreateMultiple creates multiple Droplets.
|
||||
func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *DropletMultiCreateRequest) ([]Droplet, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := dropletBasePath
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Droplets, resp, err
|
||||
}
|
||||
|
||||
// Performs a delete request given a path
|
||||
func (s *DropletsServiceOp) delete(ctx context.Context, path string) (*Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Delete Droplet.
|
||||
func (s *DropletsServiceOp) Delete(ctx context.Context, dropletID int) (*Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
|
||||
|
||||
return s.delete(ctx, path)
|
||||
}
|
||||
|
||||
// DeleteByTag deletes Droplets matched by a Tag.
|
||||
func (s *DropletsServiceOp) DeleteByTag(ctx context.Context, tag string) (*Response, error) {
|
||||
if tag == "" {
|
||||
return nil, NewArgError("tag", "cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
|
||||
|
||||
return s.delete(ctx, path)
|
||||
}
|
||||
|
||||
// Kernels lists kernels available for a Droplet.
|
||||
func (s *DropletsServiceOp) Kernels(ctx context.Context, dropletID int, opt *ListOptions) ([]Kernel, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/kernels", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(kernelsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Kernels, resp, err
|
||||
}
|
||||
|
||||
// Actions lists the actions for a Droplet.
|
||||
func (s *DropletsServiceOp) Actions(ctx context.Context, dropletID int, opt *ListOptions) ([]Action, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/actions", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
// Backups lists the backups for a Droplet.
|
||||
func (s *DropletsServiceOp) Backups(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/backups", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(backupsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Backups, resp, err
|
||||
}
|
||||
|
||||
// Snapshots lists the snapshots available for a Droplet.
|
||||
func (s *DropletsServiceOp) Snapshots(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/snapshots", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletSnapshotsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Snapshots, resp, err
|
||||
}
|
||||
|
||||
// Neighbors lists the neighbors for a Droplet.
|
||||
func (s *DropletsServiceOp) Neighbors(ctx context.Context, dropletID int) ([]Droplet, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/neighbors", dropletBasePath, dropletID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Droplets, resp, err
|
||||
}
|
||||
|
||||
func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) (string, error) {
|
||||
action, _, err := s.client.DropletActions.GetByURI(ctx, uri)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return action.Status, nil
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package godo
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ArgError is an error that represents an error with an input to godo. It
|
||||
// identifies the argument and the cause (if possible).
|
||||
type ArgError struct {
|
||||
arg string
|
||||
reason string
|
||||
}
|
||||
|
||||
var _ error = &ArgError{}
|
||||
|
||||
// NewArgError creates an InputError.
|
||||
func NewArgError(arg, reason string) *ArgError {
|
||||
return &ArgError{
|
||||
arg: arg,
|
||||
reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ArgError) Error() string {
|
||||
return fmt.Sprintf("%s is invalid because %s", e.arg, e.reason)
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const firewallsBasePath = "/v2/firewalls"
|
||||
|
||||
// FirewallsService is an interface for managing Firewalls with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#firewalls
|
||||
type FirewallsService interface {
|
||||
Get(context.Context, string) (*Firewall, *Response, error)
|
||||
Create(context.Context, *FirewallRequest) (*Firewall, *Response, error)
|
||||
Update(context.Context, string, *FirewallRequest) (*Firewall, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
List(context.Context, *ListOptions) ([]Firewall, *Response, error)
|
||||
ListByDroplet(context.Context, int, *ListOptions) ([]Firewall, *Response, error)
|
||||
AddDroplets(context.Context, string, ...int) (*Response, error)
|
||||
RemoveDroplets(context.Context, string, ...int) (*Response, error)
|
||||
AddTags(context.Context, string, ...string) (*Response, error)
|
||||
RemoveTags(context.Context, string, ...string) (*Response, error)
|
||||
AddRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
|
||||
RemoveRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
|
||||
}
|
||||
|
||||
// FirewallsServiceOp handles communication with Firewalls methods of the DigitalOcean API.
|
||||
type FirewallsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Firewall represents a DigitalOcean Firewall configuration.
|
||||
type Firewall struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
InboundRules []InboundRule `json:"inbound_rules"`
|
||||
OutboundRules []OutboundRule `json:"outbound_rules"`
|
||||
DropletIDs []int `json:"droplet_ids"`
|
||||
Tags []string `json:"tags"`
|
||||
Created string `json:"created_at"`
|
||||
PendingChanges []PendingChange `json:"pending_changes"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a Firewall.
|
||||
func (fw Firewall) String() string {
|
||||
return Stringify(fw)
|
||||
}
|
||||
|
||||
func (fw Firewall) URN() string {
|
||||
return ToURN("Firewall", fw.ID)
|
||||
}
|
||||
|
||||
// FirewallRequest represents the configuration to be applied to an existing or a new Firewall.
|
||||
type FirewallRequest struct {
|
||||
Name string `json:"name"`
|
||||
InboundRules []InboundRule `json:"inbound_rules"`
|
||||
OutboundRules []OutboundRule `json:"outbound_rules"`
|
||||
DropletIDs []int `json:"droplet_ids"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// FirewallRulesRequest represents rules configuration to be applied to an existing Firewall.
|
||||
type FirewallRulesRequest struct {
|
||||
InboundRules []InboundRule `json:"inbound_rules"`
|
||||
OutboundRules []OutboundRule `json:"outbound_rules"`
|
||||
}
|
||||
|
||||
// InboundRule represents a DigitalOcean Firewall inbound rule.
|
||||
type InboundRule struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
PortRange string `json:"ports,omitempty"`
|
||||
Sources *Sources `json:"sources"`
|
||||
}
|
||||
|
||||
// OutboundRule represents a DigitalOcean Firewall outbound rule.
|
||||
type OutboundRule struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
PortRange string `json:"ports,omitempty"`
|
||||
Destinations *Destinations `json:"destinations"`
|
||||
}
|
||||
|
||||
// Sources represents a DigitalOcean Firewall InboundRule sources.
|
||||
type Sources struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
|
||||
}
|
||||
|
||||
// PendingChange represents a DigitalOcean Firewall status details.
|
||||
type PendingChange struct {
|
||||
DropletID int `json:"droplet_id,omitempty"`
|
||||
Removing bool `json:"removing,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// Destinations represents a DigitalOcean Firewall OutboundRule destinations.
|
||||
type Destinations struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
|
||||
}
|
||||
|
||||
var _ FirewallsService = &FirewallsServiceOp{}
|
||||
|
||||
// Get an existing Firewall by its identifier.
|
||||
func (fw *FirewallsServiceOp) Get(ctx context.Context, fID string) (*Firewall, *Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID)
|
||||
|
||||
req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Firewall, resp, err
|
||||
}
|
||||
|
||||
// Create a new Firewall with a given configuration.
|
||||
func (fw *FirewallsServiceOp) Create(ctx context.Context, fr *FirewallRequest) (*Firewall, *Response, error) {
|
||||
req, err := fw.client.NewRequest(ctx, http.MethodPost, firewallsBasePath, fr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Firewall, resp, err
|
||||
}
|
||||
|
||||
// Update an existing Firewall with new configuration.
|
||||
func (fw *FirewallsServiceOp) Update(ctx context.Context, fID string, fr *FirewallRequest) (*Firewall, *Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID)
|
||||
|
||||
req, err := fw.client.NewRequest(ctx, "PUT", path, fr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Firewall, resp, err
|
||||
}
|
||||
|
||||
// Delete a Firewall by its identifier.
|
||||
func (fw *FirewallsServiceOp) Delete(ctx context.Context, fID string) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID)
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, nil)
|
||||
}
|
||||
|
||||
// List Firewalls.
|
||||
func (fw *FirewallsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Firewall, *Response, error) {
|
||||
path, err := addOptions(firewallsBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return fw.listHelper(ctx, path)
|
||||
}
|
||||
|
||||
// ListByDroplet Firewalls.
|
||||
func (fw *FirewallsServiceOp) ListByDroplet(ctx context.Context, dID int, opt *ListOptions) ([]Firewall, *Response, error) {
|
||||
basePath := path.Join(dropletBasePath, strconv.Itoa(dID), "firewalls")
|
||||
path, err := addOptions(basePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return fw.listHelper(ctx, path)
|
||||
}
|
||||
|
||||
// AddDroplets to a Firewall.
|
||||
func (fw *FirewallsServiceOp) AddDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "droplets")
|
||||
return fw.createAndDoReq(ctx, http.MethodPost, path, &dropletsRequest{IDs: dropletIDs})
|
||||
}
|
||||
|
||||
// RemoveDroplets from a Firewall.
|
||||
func (fw *FirewallsServiceOp) RemoveDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "droplets")
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, &dropletsRequest{IDs: dropletIDs})
|
||||
}
|
||||
|
||||
// AddTags to a Firewall.
|
||||
func (fw *FirewallsServiceOp) AddTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "tags")
|
||||
return fw.createAndDoReq(ctx, http.MethodPost, path, &tagsRequest{Tags: tags})
|
||||
}
|
||||
|
||||
// RemoveTags from a Firewall.
|
||||
func (fw *FirewallsServiceOp) RemoveTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "tags")
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, &tagsRequest{Tags: tags})
|
||||
}
|
||||
|
||||
// AddRules to a Firewall.
|
||||
func (fw *FirewallsServiceOp) AddRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "rules")
|
||||
return fw.createAndDoReq(ctx, http.MethodPost, path, rr)
|
||||
}
|
||||
|
||||
// RemoveRules from a Firewall.
|
||||
func (fw *FirewallsServiceOp) RemoveRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "rules")
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, rr)
|
||||
}
|
||||
|
||||
type dropletsRequest struct {
|
||||
IDs []int `json:"droplet_ids"`
|
||||
}
|
||||
|
||||
type tagsRequest struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type firewallRoot struct {
|
||||
Firewall *Firewall `json:"firewall"`
|
||||
}
|
||||
|
||||
type firewallsRoot struct {
|
||||
Firewalls []Firewall `json:"firewalls"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
func (fw *FirewallsServiceOp) createAndDoReq(ctx context.Context, method, path string, v interface{}) (*Response, error) {
|
||||
req, err := fw.client.NewRequest(ctx, method, path, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fw.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (fw *FirewallsServiceOp) listHelper(ctx context.Context, path string) ([]Firewall, *Response, error) {
|
||||
req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallsRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Firewalls, resp, err
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const floatingBasePath = "v2/floating_ips"
|
||||
|
||||
// FloatingIPsService is an interface for interfacing with the floating IPs
|
||||
// endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#floating-ips
|
||||
type FloatingIPsService interface {
|
||||
List(context.Context, *ListOptions) ([]FloatingIP, *Response, error)
|
||||
Get(context.Context, string) (*FloatingIP, *Response, error)
|
||||
Create(context.Context, *FloatingIPCreateRequest) (*FloatingIP, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// FloatingIPsServiceOp handles communication with the floating IPs related methods of the
|
||||
// DigitalOcean API.
|
||||
type FloatingIPsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ FloatingIPsService = &FloatingIPsServiceOp{}
|
||||
|
||||
// FloatingIP represents a Digital Ocean floating IP.
|
||||
type FloatingIP struct {
|
||||
Region *Region `json:"region"`
|
||||
Droplet *Droplet `json:"droplet"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func (f FloatingIP) String() string {
|
||||
return Stringify(f)
|
||||
}
|
||||
|
||||
func (f FloatingIP) URN() string {
|
||||
return ToURN("FloatingIP", f.IP)
|
||||
}
|
||||
|
||||
type floatingIPsRoot struct {
|
||||
FloatingIPs []FloatingIP `json:"floating_ips"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type floatingIPRoot struct {
|
||||
FloatingIP *FloatingIP `json:"floating_ip"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// FloatingIPCreateRequest represents a request to create a floating IP.
|
||||
// If DropletID is not empty, the floating IP will be assigned to the
|
||||
// droplet.
|
||||
type FloatingIPCreateRequest struct {
|
||||
Region string `json:"region"`
|
||||
DropletID int `json:"droplet_id,omitempty"`
|
||||
}
|
||||
|
||||
// List all floating IPs.
|
||||
func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]FloatingIP, *Response, error) {
|
||||
path := floatingBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(floatingIPsRoot)
|
||||
resp, err := f.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.FloatingIPs, resp, err
|
||||
}
|
||||
|
||||
// Get an individual floating IP.
|
||||
func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(floatingIPRoot)
|
||||
resp, err := f.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.FloatingIP, resp, err
|
||||
}
|
||||
|
||||
// Create a floating IP. If the DropletID field of the request is not empty,
|
||||
// the floating IP will also be assigned to the droplet.
|
||||
func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *FloatingIPCreateRequest) (*FloatingIP, *Response, error) {
|
||||
path := floatingBasePath
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(floatingIPRoot)
|
||||
resp, err := f.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.FloatingIP, resp, err
|
||||
}
|
||||
|
||||
// Delete a floating IP.
|
||||
func (f *FloatingIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := f.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FloatingIPActionsService is an interface for interfacing with the
|
||||
// floating IPs actions endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#floating-ips-action
|
||||
type FloatingIPActionsService interface {
|
||||
Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error)
|
||||
Unassign(ctx context.Context, ip string) (*Action, *Response, error)
|
||||
Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error)
|
||||
List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error)
|
||||
}
|
||||
|
||||
// FloatingIPActionsServiceOp handles communication with the floating IPs
|
||||
// action related methods of the DigitalOcean API.
|
||||
type FloatingIPActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Assign a floating IP to a droplet.
|
||||
func (s *FloatingIPActionsServiceOp) Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "assign",
|
||||
"droplet_id": dropletID,
|
||||
}
|
||||
return s.doAction(ctx, ip, request)
|
||||
}
|
||||
|
||||
// Unassign a floating IP from the droplet it is currently assigned to.
|
||||
func (s *FloatingIPActionsServiceOp) Unassign(ctx context.Context, ip string) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "unassign"}
|
||||
return s.doAction(ctx, ip, request)
|
||||
}
|
||||
|
||||
// Get an action for a particular floating IP by id.
|
||||
func (s *FloatingIPActionsServiceOp) Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%d", floatingIPActionPath(ip), actionID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// List the actions for a particular floating IP.
|
||||
func (s *FloatingIPActionsServiceOp) List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error) {
|
||||
path := floatingIPActionPath(ip)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) {
|
||||
path := floatingIPActionPath(ip)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *FloatingIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
func floatingIPActionPath(ip string) string {
|
||||
return fmt.Sprintf("%s/%s/actions", floatingBasePath, ip)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
module github.com/digitalocean/godo
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.3.5 // indirect
|
||||
github.com/google/go-querystring v1.0.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
)
|
||||
|
||||
replace github.com/stretchr/objx => github.com/stretchr/objx v0.2.0
|
||||
|
||||
replace golang.org/x/crypto => golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a
|
@ -0,0 +1,42 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
@ -0,0 +1,432 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
libraryVersion = "1.37.0"
|
||||
defaultBaseURL = "https://api.digitalocean.com/"
|
||||
userAgent = "godo/" + libraryVersion
|
||||
mediaType = "application/json"
|
||||
|
||||
headerRateLimit = "RateLimit-Limit"
|
||||
headerRateRemaining = "RateLimit-Remaining"
|
||||
headerRateReset = "RateLimit-Reset"
|
||||
)
|
||||
|
||||
// Client manages communication with DigitalOcean V2 API.
|
||||
type Client struct {
|
||||
// HTTP client used to communicate with the DO API.
|
||||
client *http.Client
|
||||
|
||||
// Base URL for API requests.
|
||||
BaseURL *url.URL
|
||||
|
||||
// User agent for client
|
||||
UserAgent string
|
||||
|
||||
// Rate contains the current rate limit for the client as determined by the most recent
|
||||
// API call.
|
||||
Rate Rate
|
||||
|
||||
// Services used for communicating with the API
|
||||
Account AccountService
|
||||
Actions ActionsService
|
||||
Balance BalanceService
|
||||
BillingHistory BillingHistoryService
|
||||
CDNs CDNService
|
||||
Domains DomainsService
|
||||
Droplets DropletsService
|
||||
DropletActions DropletActionsService
|
||||
Images ImagesService
|
||||
ImageActions ImageActionsService
|
||||
Invoices InvoicesService
|
||||
Keys KeysService
|
||||
Regions RegionsService
|
||||
Sizes SizesService
|
||||
FloatingIPs FloatingIPsService
|
||||
FloatingIPActions FloatingIPActionsService
|
||||
Snapshots SnapshotsService
|
||||
Storage StorageService
|
||||
StorageActions StorageActionsService
|
||||
Tags TagsService
|
||||
LoadBalancers LoadBalancersService
|
||||
Certificates CertificatesService
|
||||
Firewalls FirewallsService
|
||||
Projects ProjectsService
|
||||
Kubernetes KubernetesService
|
||||
Registry RegistryService
|
||||
Databases DatabasesService
|
||||
VPCs VPCsService
|
||||
OneClick OneClickService
|
||||
|
||||
// Optional function called after every successful request made to the DO APIs
|
||||
onRequestCompleted RequestCompletionCallback
|
||||
}
|
||||
|
||||
// RequestCompletionCallback defines the type of the request callback function
|
||||
type RequestCompletionCallback func(*http.Request, *http.Response)
|
||||
|
||||
// ListOptions specifies the optional parameters to various List methods that
|
||||
// support pagination.
|
||||
type ListOptions struct {
|
||||
// For paginated result sets, page of results to retrieve.
|
||||
Page int `url:"page,omitempty"`
|
||||
|
||||
// For paginated result sets, the number of results to include per page.
|
||||
PerPage int `url:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
// Response is a DigitalOcean response. This wraps the standard http.Response returned from DigitalOcean.
|
||||
type Response struct {
|
||||
*http.Response
|
||||
|
||||
// Links that were returned with the response. These are parsed from
|
||||
// request body and not the header.
|
||||
Links *Links
|
||||
|
||||
// Meta describes generic information about the response.
|
||||
Meta *Meta
|
||||
|
||||
// Monitoring URI
|
||||
Monitor string
|
||||
|
||||
Rate
|
||||
}
|
||||
|
||||
// An ErrorResponse reports the error caused by an API request
|
||||
type ErrorResponse struct {
|
||||
// HTTP response that caused this error
|
||||
Response *http.Response
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
|
||||
// RequestID returned from the API, useful to contact support.
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
// Rate contains the rate limit for the current client.
|
||||
type Rate struct {
|
||||
// The number of request per hour the client is currently limited to.
|
||||
Limit int `json:"limit"`
|
||||
|
||||
// The number of remaining requests the client can make this hour.
|
||||
Remaining int `json:"remaining"`
|
||||
|
||||
// The time at which the current rate limit will reset.
|
||||
Reset Timestamp `json:"reset"`
|
||||
}
|
||||
|
||||
func addOptions(s string, opt interface{}) (string, error) {
|
||||
v := reflect.ValueOf(opt)
|
||||
|
||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
origURL, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
origValues := origURL.Query()
|
||||
|
||||
newValues, err := query.Values(opt)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
for k, v := range newValues {
|
||||
origValues[k] = v
|
||||
}
|
||||
|
||||
origURL.RawQuery = origValues.Encode()
|
||||
return origURL.String(), nil
|
||||
}
|
||||
|
||||
// NewFromToken returns a new DigitalOcean API client with the given API
|
||||
// token.
|
||||
func NewFromToken(token string) *Client {
|
||||
ctx := context.Background()
|
||||
|
||||
config := &oauth2.Config{}
|
||||
ts := config.TokenSource(ctx, &oauth2.Token{AccessToken: token})
|
||||
|
||||
return NewClient(oauth2.NewClient(ctx, ts))
|
||||
}
|
||||
|
||||
// NewClient returns a new DigitalOcean API client, using the given
|
||||
// http.Client to perform all requests.
|
||||
//
|
||||
// Users who wish to pass their own http.Client should use this method. If
|
||||
// you're in need of further customization, the godo.New method allows more
|
||||
// options, such as setting a custom URL or a custom user agent string.
|
||||
func NewClient(httpClient *http.Client) *Client {
|
||||
if httpClient == nil {
|
||||
httpClient = http.DefaultClient
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
|
||||
c.Account = &AccountServiceOp{client: c}
|
||||
c.Actions = &ActionsServiceOp{client: c}
|
||||
c.Balance = &BalanceServiceOp{client: c}
|
||||
c.BillingHistory = &BillingHistoryServiceOp{client: c}
|
||||
c.CDNs = &CDNServiceOp{client: c}
|
||||
c.Certificates = &CertificatesServiceOp{client: c}
|
||||
c.Domains = &DomainsServiceOp{client: c}
|
||||
c.Droplets = &DropletsServiceOp{client: c}
|
||||
c.DropletActions = &DropletActionsServiceOp{client: c}
|
||||
c.Firewalls = &FirewallsServiceOp{client: c}
|
||||
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
|
||||
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
|
||||
c.Images = &ImagesServiceOp{client: c}
|
||||
c.ImageActions = &ImageActionsServiceOp{client: c}
|
||||
c.Invoices = &InvoicesServiceOp{client: c}
|
||||
c.Keys = &KeysServiceOp{client: c}
|
||||
c.LoadBalancers = &LoadBalancersServiceOp{client: c}
|
||||
c.Projects = &ProjectsServiceOp{client: c}
|
||||
c.Regions = &RegionsServiceOp{client: c}
|
||||
c.Sizes = &SizesServiceOp{client: c}
|
||||
c.Snapshots = &SnapshotsServiceOp{client: c}
|
||||
c.Storage = &StorageServiceOp{client: c}
|
||||
c.StorageActions = &StorageActionsServiceOp{client: c}
|
||||
c.Tags = &TagsServiceOp{client: c}
|
||||
c.Kubernetes = &KubernetesServiceOp{client: c}
|
||||
c.Registry = &RegistryServiceOp{client: c}
|
||||
c.Databases = &DatabasesServiceOp{client: c}
|
||||
c.VPCs = &VPCsServiceOp{client: c}
|
||||
c.OneClick = &OneClickServiceOp{client: c}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// ClientOpt are options for New.
|
||||
type ClientOpt func(*Client) error
|
||||
|
||||
// New returns a new DigitalOcean API client instance.
|
||||
func New(httpClient *http.Client, opts ...ClientOpt) (*Client, error) {
|
||||
c := NewClient(httpClient)
|
||||
for _, opt := range opts {
|
||||
if err := opt(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// SetBaseURL is a client option for setting the base URL.
|
||||
func SetBaseURL(bu string) ClientOpt {
|
||||
return func(c *Client) error {
|
||||
u, err := url.Parse(bu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.BaseURL = u
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetUserAgent is a client option for setting the user agent.
|
||||
func SetUserAgent(ua string) ClientOpt {
|
||||
return func(c *Client) error {
|
||||
c.UserAgent = fmt.Sprintf("%s %s", ua, c.UserAgent)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequest creates an API request. A relative URL can be provided in urlStr, which will be resolved to the
|
||||
// BaseURL of the Client. Relative URLS should always be specified without a preceding slash. If specified, the
|
||||
// value pointed to by body is JSON encoded and included in as the request body.
|
||||
func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
|
||||
u, err := c.BaseURL.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if body != nil {
|
||||
err = json.NewEncoder(buf).Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", mediaType)
|
||||
req.Header.Add("Accept", mediaType)
|
||||
req.Header.Add("User-Agent", c.UserAgent)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// OnRequestCompleted sets the DO API request completion callback
|
||||
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
|
||||
c.onRequestCompleted = rc
|
||||
}
|
||||
|
||||
// newResponse creates a new Response for the provided http.Response
|
||||
func newResponse(r *http.Response) *Response {
|
||||
response := Response{Response: r}
|
||||
response.populateRate()
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// populateRate parses the rate related headers and populates the response Rate.
|
||||
func (r *Response) populateRate() {
|
||||
if limit := r.Header.Get(headerRateLimit); limit != "" {
|
||||
r.Rate.Limit, _ = strconv.Atoi(limit)
|
||||
}
|
||||
if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
|
||||
r.Rate.Remaining, _ = strconv.Atoi(remaining)
|
||||
}
|
||||
if reset := r.Header.Get(headerRateReset); reset != "" {
|
||||
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
|
||||
r.Rate.Reset = Timestamp{time.Unix(v, 0)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
|
||||
// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
|
||||
// the raw response will be written to v, without attempting to decode it.
|
||||
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
|
||||
resp, err := DoRequestWithClient(ctx, c.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.onRequestCompleted != nil {
|
||||
c.onRequestCompleted(req, resp)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if rerr := resp.Body.Close(); err == nil {
|
||||
err = rerr
|
||||
}
|
||||
}()
|
||||
|
||||
response := newResponse(resp)
|
||||
c.Rate = response.Rate
|
||||
|
||||
err = CheckResponse(resp)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
if w, ok := v.(io.Writer); ok {
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
err = json.NewDecoder(resp.Body).Decode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
// DoRequest submits an HTTP request.
|
||||
func DoRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
return DoRequestWithClient(ctx, http.DefaultClient, req)
|
||||
}
|
||||
|
||||
// DoRequestWithClient submits an HTTP request using the specified client.
|
||||
func DoRequestWithClient(
|
||||
ctx context.Context,
|
||||
client *http.Client,
|
||||
req *http.Request) (*http.Response, error) {
|
||||
req = req.WithContext(ctx)
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func (r *ErrorResponse) Error() string {
|
||||
if r.RequestID != "" {
|
||||
return fmt.Sprintf("%v %v: %d (request %q) %v",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message)
|
||||
}
|
||||
return fmt.Sprintf("%v %v: %d %v",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message)
|
||||
}
|
||||
|
||||
// CheckResponse checks the API response for errors, and returns them if present. A response is considered an
|
||||
// error if it has a status code outside the 200 range. API error responses are expected to have either no response
|
||||
// body, or a JSON response body that maps to ErrorResponse. Any other response body will be silently ignored.
|
||||
func CheckResponse(r *http.Response) error {
|
||||
if c := r.StatusCode; c >= 200 && c <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorResponse := &ErrorResponse{Response: r}
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err == nil && len(data) > 0 {
|
||||
err := json.Unmarshal(data, errorResponse)
|
||||
if err != nil {
|
||||
errorResponse.Message = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
func (r Rate) String() string {
|
||||
return Stringify(r)
|
||||
}
|
||||
|
||||
// String is a helper routine that allocates a new string value
|
||||
// to store v and returns a pointer to it.
|
||||
func String(v string) *string {
|
||||
p := new(string)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Int is a helper routine that allocates a new int32 value
|
||||
// to store v and returns a pointer to it, but unlike Int32
|
||||
// its argument value is an int.
|
||||
func Int(v int) *int {
|
||||
p := new(int)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Bool is a helper routine that allocates a new bool value
|
||||
// to store v and returns a pointer to it.
|
||||
func Bool(v bool) *bool {
|
||||
p := new(bool)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// StreamToString converts a reader to a string
|
||||
func StreamToString(stream io.Reader) string {
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(stream)
|
||||
return buf.String()
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ImageActionsService is an interface for interfacing with the image actions
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#image-actions
|
||||
type ImageActionsService interface {
|
||||
Get(context.Context, int, int) (*Action, *Response, error)
|
||||
Transfer(context.Context, int, *ActionRequest) (*Action, *Response, error)
|
||||
Convert(context.Context, int) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// ImageActionsServiceOp handles communition with the image action related methods of the
|
||||
// DigitalOcean API.
|
||||
type ImageActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ ImageActionsService = &ImageActionsServiceOp{}
|
||||
|
||||
// Transfer an image
|
||||
func (i *ImageActionsServiceOp) Transfer(ctx context.Context, imageID int, transferRequest *ActionRequest) (*Action, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if transferRequest == nil {
|
||||
return nil, nil, NewArgError("transferRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("v2/images/%d/actions", imageID)
|
||||
|
||||
req, err := i.client.NewRequest(ctx, http.MethodPost, path, transferRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := i.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
// Convert an image to a snapshot
|
||||
func (i *ImageActionsServiceOp) Convert(ctx context.Context, imageID int) (*Action, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannont be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("v2/images/%d/actions", imageID)
|
||||
|
||||
convertRequest := &ActionRequest{
|
||||
"type": "convert",
|
||||
}
|
||||
|
||||
req, err := i.client.NewRequest(ctx, http.MethodPost, path, convertRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := i.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
// Get an action for a particular image by id.
|
||||
func (i *ImageActionsServiceOp) Get(ctx context.Context, imageID, actionID int) (*Action, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if actionID < 1 {
|
||||
return nil, nil, NewArgError("actionID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID)
|
||||
|
||||
req, err := i.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := i.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
@ -0,0 +1,245 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const imageBasePath = "v2/images"
|
||||
|
||||
// ImagesService is an interface for interfacing with the images
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#images
|
||||
type ImagesService interface {
|
||||
List(context.Context, *ListOptions) ([]Image, *Response, error)
|
||||
ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
|
||||
ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
|
||||
ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
|
||||
ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error)
|
||||
GetByID(context.Context, int) (*Image, *Response, error)
|
||||
GetBySlug(context.Context, string) (*Image, *Response, error)
|
||||
Create(context.Context, *CustomImageCreateRequest) (*Image, *Response, error)
|
||||
Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error)
|
||||
Delete(context.Context, int) (*Response, error)
|
||||
}
|
||||
|
||||
// ImagesServiceOp handles communication with the image related methods of the
|
||||
// DigitalOcean API.
|
||||
type ImagesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ ImagesService = &ImagesServiceOp{}
|
||||
|
||||
// Image represents a DigitalOcean Image
|
||||
type Image struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Distribution string `json:"distribution,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Public bool `json:"public,omitempty"`
|
||||
Regions []string `json:"regions,omitempty"`
|
||||
MinDiskSize int `json:"min_disk_size,omitempty"`
|
||||
SizeGigaBytes float64 `json:"size_gigabytes,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
// ImageUpdateRequest represents a request to update an image.
|
||||
type ImageUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CustomImageCreateRequest represents a request to create a custom image.
|
||||
type CustomImageCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
Region string `json:"region"`
|
||||
Distribution string `json:"distribution,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type imageRoot struct {
|
||||
Image *Image
|
||||
}
|
||||
|
||||
type imagesRoot struct {
|
||||
Images []Image
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type listImageOptions struct {
|
||||
Private bool `url:"private,omitempty"`
|
||||
Type string `url:"type,omitempty"`
|
||||
Tag string `url:"tag_name,omitempty"`
|
||||
}
|
||||
|
||||
func (i Image) String() string {
|
||||
return Stringify(i)
|
||||
}
|
||||
|
||||
// List lists all the images available.
|
||||
func (s *ImagesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
return s.list(ctx, opt, nil)
|
||||
}
|
||||
|
||||
// ListDistribution lists all the distribution images.
|
||||
func (s *ImagesServiceOp) ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
listOpt := listImageOptions{Type: "distribution"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// ListApplication lists all the application images.
|
||||
func (s *ImagesServiceOp) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
listOpt := listImageOptions{Type: "application"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// ListUser lists all the user images.
|
||||
func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
listOpt := listImageOptions{Private: true}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// ListByTag lists all images with a specific tag applied.
|
||||
func (s *ImagesServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) {
|
||||
listOpt := listImageOptions{Tag: tag}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// GetByID retrieves an image by id.
|
||||
func (s *ImagesServiceOp) GetByID(ctx context.Context, imageID int) (*Image, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
return s.get(ctx, interface{}(imageID))
|
||||
}
|
||||
|
||||
// GetBySlug retrieves an image by slug.
|
||||
func (s *ImagesServiceOp) GetBySlug(ctx context.Context, slug string) (*Image, *Response, error) {
|
||||
if len(slug) < 1 {
|
||||
return nil, nil, NewArgError("slug", "cannot be blank")
|
||||
}
|
||||
|
||||
return s.get(ctx, interface{}(slug))
|
||||
}
|
||||
|
||||
func (s *ImagesServiceOp) Create(ctx context.Context, createRequest *CustomImageCreateRequest) (*Image, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, imageBasePath, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(imageRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Image, resp, err
|
||||
}
|
||||
|
||||
// Update an image name.
|
||||
func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if updateRequest == nil {
|
||||
return nil, nil, NewArgError("updateRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPut, path, updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(imageRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Image, resp, err
|
||||
}
|
||||
|
||||
// Delete an image.
|
||||
func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Helper method for getting an individual image
|
||||
func (s *ImagesServiceOp) get(ctx context.Context, ID interface{}) (*Image, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%v", imageBasePath, ID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(imageRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Image, resp, err
|
||||
}
|
||||
|
||||
// Helper method for listing images
|
||||
func (s *ImagesServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listImageOptions) ([]Image, *Response, error) {
|
||||
path := imageBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
path, err = addOptions(path, listOpt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(imagesRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Images, resp, err
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const invoicesBasePath = "v2/customers/my/invoices"
|
||||
|
||||
// InvoicesService is an interface for interfacing with the Invoice
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#invoices
|
||||
type InvoicesService interface {
|
||||
Get(context.Context, string, *ListOptions) (*Invoice, *Response, error)
|
||||
GetPDF(context.Context, string) ([]byte, *Response, error)
|
||||
GetCSV(context.Context, string) ([]byte, *Response, error)
|
||||
List(context.Context, *ListOptions) (*InvoiceList, *Response, error)
|
||||
GetSummary(context.Context, string) (*InvoiceSummary, *Response, error)
|
||||
}
|
||||
|
||||
// InvoicesServiceOp handles communication with the Invoice related methods of
|
||||
// the DigitalOcean API.
|
||||
type InvoicesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ InvoicesService = &InvoicesServiceOp{}
|
||||
|
||||
// Invoice represents a DigitalOcean Invoice
|
||||
type Invoice struct {
|
||||
InvoiceItems []InvoiceItem `json:"invoice_items"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// InvoiceItem represents a line-item on a DigitalOcean Invoice
|
||||
type InvoiceItem struct {
|
||||
Product string `json:"product"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
ResourceUUID string `json:"resource_uuid"`
|
||||
GroupDescription string `json:"group_description"`
|
||||
Description string `json:"description"`
|
||||
Amount string `json:"amount"`
|
||||
Duration string `json:"duration"`
|
||||
DurationUnit string `json:"duration_unit"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
ProjectName string `json:"project_name"`
|
||||
}
|
||||
|
||||
// InvoiceList contains a paginated list of all of a customer's invoices.
|
||||
// The InvoicePreview is the month-to-date usage generated by DigitalOcean.
|
||||
type InvoiceList struct {
|
||||
Invoices []InvoiceListItem `json:"invoices"`
|
||||
InvoicePreview InvoiceListItem `json:"invoice_preview"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// InvoiceListItem contains a small list of information about a customer's invoice.
|
||||
// More information can be found in the Invoice or InvoiceSummary
|
||||
type InvoiceListItem struct {
|
||||
InvoiceUUID string `json:"invoice_uuid"`
|
||||
Amount string `json:"amount"`
|
||||
InvoicePeriod string `json:"invoice_period"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// InvoiceSummary contains metadata and summarized usage for an invoice generated by DigitalOcean
|
||||
type InvoiceSummary struct {
|
||||
InvoiceUUID string `json:"invoice_uuid"`
|
||||
BillingPeriod string `json:"billing_period"`
|
||||
Amount string `json:"amount"`
|
||||
UserName string `json:"user_name"`
|
||||
UserBillingAddress Address `json:"user_billing_address"`
|
||||
UserCompany string `json:"user_company"`
|
||||
UserEmail string `json:"user_email"`
|
||||
ProductCharges InvoiceSummaryBreakdown `json:"product_charges"`
|
||||
Overages InvoiceSummaryBreakdown `json:"overages"`
|
||||
Taxes InvoiceSummaryBreakdown `json:"taxes"`
|
||||
CreditsAndAdjustments InvoiceSummaryBreakdown `json:"credits_and_adjustments"`
|
||||
}
|
||||
|
||||
// Address represents the billing address of a customer
|
||||
type Address struct {
|
||||
AddressLine1 string `json:"address_line1"`
|
||||
AddressLine2 string `json:"address_line2"`
|
||||
City string `json:"city"`
|
||||
Region string `json:"region"`
|
||||
PostalCode string `json:"postal_code"`
|
||||
CountryISO2Code string `json:"country_iso2_code"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// InvoiceSummaryBreakdown is a grouped set of InvoiceItems from an invoice
|
||||
type InvoiceSummaryBreakdown struct {
|
||||
Name string `json:"name"`
|
||||
Amount string `json:"amount"`
|
||||
Items []InvoiceSummaryBreakdownItem `json:"items"`
|
||||
}
|
||||
|
||||
// InvoiceSummaryBreakdownItem further breaks down the InvoiceSummary by product
|
||||
type InvoiceSummaryBreakdownItem struct {
|
||||
Name string `json:"name"`
|
||||
Amount string `json:"amount"`
|
||||
Count string `json:"count"`
|
||||
}
|
||||
|
||||
func (i Invoice) String() string {
|
||||
return Stringify(i)
|
||||
}
|
||||
|
||||
// Get detailed invoice items for an Invoice
|
||||
func (s *InvoicesServiceOp) Get(ctx context.Context, invoiceUUID string, opt *ListOptions) (*Invoice, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", invoicesBasePath, invoiceUUID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Invoice)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root, resp, err
|
||||
}
|
||||
|
||||
// List invoices for a customer
|
||||
func (s *InvoicesServiceOp) List(ctx context.Context, opt *ListOptions) (*InvoiceList, *Response, error) {
|
||||
path := invoicesBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(InvoiceList)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root, resp, err
|
||||
}
|
||||
|
||||
// Get a summary of metadata and summarized usage for an Invoice
|
||||
func (s *InvoicesServiceOp) GetSummary(ctx context.Context, invoiceUUID string) (*InvoiceSummary, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/summary", invoicesBasePath, invoiceUUID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(InvoiceSummary)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, err
|
||||
}
|
||||
|
||||
// Get the pdf for an Invoice
|
||||
func (s *InvoicesServiceOp) GetPDF(ctx context.Context, invoiceUUID string) ([]byte, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/pdf", invoicesBasePath, invoiceUUID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var root bytes.Buffer
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Bytes(), resp, err
|
||||
}
|
||||
|
||||
// Get the csv for an Invoice
|
||||
func (s *InvoicesServiceOp) GetCSV(ctx context.Context, invoiceUUID string) ([]byte, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/csv", invoicesBasePath, invoiceUUID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var root bytes.Buffer
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Bytes(), resp, err
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const keysBasePath = "v2/account/keys"
|
||||
|
||||
// KeysService is an interface for interfacing with the keys
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#keys
|
||||
type KeysService interface {
|
||||
List(context.Context, *ListOptions) ([]Key, *Response, error)
|
||||
GetByID(context.Context, int) (*Key, *Response, error)
|
||||
GetByFingerprint(context.Context, string) (*Key, *Response, error)
|
||||
Create(context.Context, *KeyCreateRequest) (*Key, *Response, error)
|
||||
UpdateByID(context.Context, int, *KeyUpdateRequest) (*Key, *Response, error)
|
||||
UpdateByFingerprint(context.Context, string, *KeyUpdateRequest) (*Key, *Response, error)
|
||||
DeleteByID(context.Context, int) (*Response, error)
|
||||
DeleteByFingerprint(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// KeysServiceOp handles communication with key related method of the
|
||||
// DigitalOcean API.
|
||||
type KeysServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ KeysService = &KeysServiceOp{}
|
||||
|
||||
// Key represents a DigitalOcean Key.
|
||||
type Key struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
}
|
||||
|
||||
// KeyUpdateRequest represents a request to update a DigitalOcean key.
|
||||
type KeyUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type keysRoot struct {
|
||||
SSHKeys []Key `json:"ssh_keys"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type keyRoot struct {
|
||||
SSHKey *Key `json:"ssh_key"`
|
||||
}
|
||||
|
||||
func (s Key) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
// KeyCreateRequest represents a request to create a new key.
|
||||
type KeyCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
// List all keys
|
||||
func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Response, error) {
|
||||
path := keysBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keysRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.SSHKeys, resp, err
|
||||
}
|
||||
|
||||
// Performs a get given a path
|
||||
func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// GetByID gets a Key by id
|
||||
func (s *KeysServiceOp) GetByID(ctx context.Context, keyID int) (*Key, *Response, error) {
|
||||
if keyID < 1 {
|
||||
return nil, nil, NewArgError("keyID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// GetByFingerprint gets a Key by by fingerprint
|
||||
func (s *KeysServiceOp) GetByFingerprint(ctx context.Context, fingerprint string) (*Key, *Response, error) {
|
||||
if len(fingerprint) < 1 {
|
||||
return nil, nil, NewArgError("fingerprint", "cannot not be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// Create a key using a KeyCreateRequest
|
||||
func (s *KeysServiceOp) Create(ctx context.Context, createRequest *KeyCreateRequest) (*Key, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, keysBasePath, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// UpdateByID updates a key name by ID.
|
||||
func (s *KeysServiceOp) UpdateByID(ctx context.Context, keyID int, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
|
||||
if keyID < 1 {
|
||||
return nil, nil, NewArgError("keyID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if updateRequest == nil {
|
||||
return nil, nil, NewArgError("updateRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
|
||||
req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// UpdateByFingerprint updates a key name by fingerprint.
|
||||
func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint string, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
|
||||
if len(fingerprint) < 1 {
|
||||
return nil, nil, NewArgError("fingerprint", "cannot be empty")
|
||||
}
|
||||
|
||||
if updateRequest == nil {
|
||||
return nil, nil, NewArgError("updateRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
|
||||
req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// Delete key using a path
|
||||
func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// DeleteByID deletes a key by its id
|
||||
func (s *KeysServiceOp) DeleteByID(ctx context.Context, keyID int) (*Response, error) {
|
||||
if keyID < 1 {
|
||||
return nil, NewArgError("keyID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
|
||||
return s.delete(ctx, path)
|
||||
}
|
||||
|
||||
// DeleteByFingerprint deletes a key by its fingerprint
|
||||
func (s *KeysServiceOp) DeleteByFingerprint(ctx context.Context, fingerprint string) (*Response, error) {
|
||||
if len(fingerprint) < 1 {
|
||||
return nil, NewArgError("fingerprint", "cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
|
||||
return s.delete(ctx, path)
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Links manages links that are returned along with a List
|
||||
type Links struct {
|
||||
Pages *Pages `json:"pages,omitempty"`
|
||||
Actions []LinkAction `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
// Pages are pages specified in Links
|
||||
type Pages struct {
|
||||
First string `json:"first,omitempty"`
|
||||
Prev string `json:"prev,omitempty"`
|
||||
Last string `json:"last,omitempty"`
|
||||
Next string `json:"next,omitempty"`
|
||||
}
|
||||
|
||||
// LinkAction is a pointer to an action
|
||||
type LinkAction struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Rel string `json:"rel,omitempty"`
|
||||
HREF string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
// CurrentPage is current page of the list
|
||||
func (l *Links) CurrentPage() (int, error) {
|
||||
return l.Pages.current()
|
||||
}
|
||||
|
||||
func (p *Pages) current() (int, error) {
|
||||
switch {
|
||||
case p == nil:
|
||||
return 1, nil
|
||||
case p.Prev == "" && p.Next != "":
|
||||
return 1, nil
|
||||
case p.Prev != "":
|
||||
prevPage, err := pageForURL(p.Prev)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return prevPage + 1, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// IsLastPage returns true if the current page is the last
|
||||
func (l *Links) IsLastPage() bool {
|
||||
if l.Pages == nil {
|
||||
return true
|
||||
}
|
||||
return l.Pages.isLast()
|
||||
}
|
||||
|
||||
func (p *Pages) isLast() bool {
|
||||
return p.Next == ""
|
||||
}
|
||||
|
||||
func pageForURL(urlText string) (int, error) {
|
||||
u, err := url.ParseRequestURI(urlText)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
pageStr := u.Query().Get("page")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// Get a link action by id.
|
||||
func (la *LinkAction) Get(ctx context.Context, client *Client) (*Action, *Response, error) {
|
||||
return client.Actions.Get(ctx, la.ID)
|
||||
}
|
@ -0,0 +1,324 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const loadBalancersBasePath = "/v2/load_balancers"
|
||||
const forwardingRulesPath = "forwarding_rules"
|
||||
|
||||
const dropletsPath = "droplets"
|
||||
|
||||
// LoadBalancersService is an interface for managing load balancers with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#load-balancers
|
||||
type LoadBalancersService interface {
|
||||
Get(context.Context, string) (*LoadBalancer, *Response, error)
|
||||
List(context.Context, *ListOptions) ([]LoadBalancer, *Response, error)
|
||||
Create(context.Context, *LoadBalancerRequest) (*LoadBalancer, *Response, error)
|
||||
Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error)
|
||||
Delete(ctx context.Context, lbID string) (*Response, error)
|
||||
AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
|
||||
RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
|
||||
AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
|
||||
RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
|
||||
}
|
||||
|
||||
// LoadBalancer represents a DigitalOcean load balancer configuration.
|
||||
// Tags can only be provided upon the creation of a Load Balancer.
|
||||
type LoadBalancer struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
|
||||
HealthCheck *HealthCheck `json:"health_check,omitempty"`
|
||||
StickySessions *StickySessions `json:"sticky_sessions,omitempty"`
|
||||
Region *Region `json:"region,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"`
|
||||
EnableProxyProtocol bool `json:"enable_proxy_protocol,omitempty"`
|
||||
EnableBackendKeepalive bool `json:"enable_backend_keepalive,omitempty"`
|
||||
VPCUUID string `json:"vpc_uuid,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a LoadBalancer.
|
||||
func (l LoadBalancer) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
func (l LoadBalancer) URN() string {
|
||||
return ToURN("LoadBalancer", l.ID)
|
||||
}
|
||||
|
||||
// AsRequest creates a LoadBalancerRequest that can be submitted to Update with the current values of the LoadBalancer.
|
||||
// Modifying the returned LoadBalancerRequest will not modify the original LoadBalancer.
|
||||
func (l LoadBalancer) AsRequest() *LoadBalancerRequest {
|
||||
r := LoadBalancerRequest{
|
||||
Name: l.Name,
|
||||
Algorithm: l.Algorithm,
|
||||
ForwardingRules: append([]ForwardingRule(nil), l.ForwardingRules...),
|
||||
DropletIDs: append([]int(nil), l.DropletIDs...),
|
||||
Tag: l.Tag,
|
||||
RedirectHttpToHttps: l.RedirectHttpToHttps,
|
||||
EnableProxyProtocol: l.EnableProxyProtocol,
|
||||
EnableBackendKeepalive: l.EnableBackendKeepalive,
|
||||
HealthCheck: l.HealthCheck,
|
||||
VPCUUID: l.VPCUUID,
|
||||
}
|
||||
|
||||
if l.HealthCheck != nil {
|
||||
r.HealthCheck = &HealthCheck{}
|
||||
*r.HealthCheck = *l.HealthCheck
|
||||
}
|
||||
if l.StickySessions != nil {
|
||||
r.StickySessions = &StickySessions{}
|
||||
*r.StickySessions = *l.StickySessions
|
||||
}
|
||||
if l.Region != nil {
|
||||
r.Region = l.Region.Slug
|
||||
}
|
||||
return &r
|
||||
}
|
||||
|
||||
// ForwardingRule represents load balancer forwarding rules.
|
||||
type ForwardingRule struct {
|
||||
EntryProtocol string `json:"entry_protocol,omitempty"`
|
||||
EntryPort int `json:"entry_port,omitempty"`
|
||||
TargetProtocol string `json:"target_protocol,omitempty"`
|
||||
TargetPort int `json:"target_port,omitempty"`
|
||||
CertificateID string `json:"certificate_id,omitempty"`
|
||||
TlsPassthrough bool `json:"tls_passthrough,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a ForwardingRule.
|
||||
func (f ForwardingRule) String() string {
|
||||
return Stringify(f)
|
||||
}
|
||||
|
||||
// HealthCheck represents optional load balancer health check rules.
|
||||
type HealthCheck struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
CheckIntervalSeconds int `json:"check_interval_seconds,omitempty"`
|
||||
ResponseTimeoutSeconds int `json:"response_timeout_seconds,omitempty"`
|
||||
HealthyThreshold int `json:"healthy_threshold,omitempty"`
|
||||
UnhealthyThreshold int `json:"unhealthy_threshold,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a HealthCheck.
|
||||
func (h HealthCheck) String() string {
|
||||
return Stringify(h)
|
||||
}
|
||||
|
||||
// StickySessions represents optional load balancer session affinity rules.
|
||||
type StickySessions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
CookieName string `json:"cookie_name,omitempty"`
|
||||
CookieTtlSeconds int `json:"cookie_ttl_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a StickySessions instance.
|
||||
func (s StickySessions) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
// LoadBalancerRequest represents the configuration to be applied to an existing or a new load balancer.
|
||||
type LoadBalancerRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
|
||||
HealthCheck *HealthCheck `json:"health_check,omitempty"`
|
||||
StickySessions *StickySessions `json:"sticky_sessions,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"`
|
||||
EnableProxyProtocol bool `json:"enable_proxy_protocol,omitempty"`
|
||||
EnableBackendKeepalive bool `json:"enable_backend_keepalive,omitempty"`
|
||||
VPCUUID string `json:"vpc_uuid,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a LoadBalancerRequest.
|
||||
func (l LoadBalancerRequest) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
type forwardingRulesRequest struct {
|
||||
Rules []ForwardingRule `json:"forwarding_rules,omitempty"`
|
||||
}
|
||||
|
||||
func (l forwardingRulesRequest) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
type dropletIDsRequest struct {
|
||||
IDs []int `json:"droplet_ids,omitempty"`
|
||||
}
|
||||
|
||||
func (l dropletIDsRequest) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
type loadBalancersRoot struct {
|
||||
LoadBalancers []LoadBalancer `json:"load_balancers"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type loadBalancerRoot struct {
|
||||
LoadBalancer *LoadBalancer `json:"load_balancer"`
|
||||
}
|
||||
|
||||
// LoadBalancersServiceOp handles communication with load balancer-related methods of the DigitalOcean API.
|
||||
type LoadBalancersServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ LoadBalancersService = &LoadBalancersServiceOp{}
|
||||
|
||||
// Get an existing load balancer by its identifier.
|
||||
func (l *LoadBalancersServiceOp) Get(ctx context.Context, lbID string) (*LoadBalancer, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancerRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.LoadBalancer, resp, err
|
||||
}
|
||||
|
||||
// List load balancers, with optional pagination.
|
||||
func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([]LoadBalancer, *Response, error) {
|
||||
path, err := addOptions(loadBalancersBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancersRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.LoadBalancers, resp, err
|
||||
}
|
||||
|
||||
// Create a new load balancer with a given configuration.
|
||||
func (l *LoadBalancersServiceOp) Create(ctx context.Context, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
|
||||
req, err := l.client.NewRequest(ctx, http.MethodPost, loadBalancersBasePath, lbr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancerRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.LoadBalancer, resp, err
|
||||
}
|
||||
|
||||
// Update an existing load balancer with new configuration.
|
||||
func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, "PUT", path, lbr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancerRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.LoadBalancer, resp, err
|
||||
}
|
||||
|
||||
// Delete a load balancer by its identifier.
|
||||
func (l *LoadBalancersServiceOp) Delete(ctx context.Context, ldID string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, ldID)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// AddDroplets adds droplets to a load balancer.
|
||||
func (l *LoadBalancersServiceOp) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodPost, path, &dropletIDsRequest{IDs: dropletIDs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemoveDroplets removes droplets from a load balancer.
|
||||
func (l *LoadBalancersServiceOp) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &dropletIDsRequest{IDs: dropletIDs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// AddForwardingRules adds forwarding rules to a load balancer.
|
||||
func (l *LoadBalancersServiceOp) AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodPost, path, &forwardingRulesRequest{Rules: rules})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemoveForwardingRules removes forwarding rules from a load balancer.
|
||||
func (l *LoadBalancersServiceOp) RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &forwardingRulesRequest{Rules: rules})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package godo
|
||||
|
||||
// Meta describes generic information about a response.
|
||||
type Meta struct {
|
||||
Total int `json:"total"`
|
||||
}
|
@ -0,0 +1,310 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultProject is the ID you should use if you are working with your
|
||||
// default project.
|
||||
DefaultProject = "default"
|
||||
|
||||
projectsBasePath = "/v2/projects"
|
||||
)
|
||||
|
||||
// ProjectsService is an interface for creating and managing Projects with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#projects
|
||||
type ProjectsService interface {
|
||||
List(context.Context, *ListOptions) ([]Project, *Response, error)
|
||||
GetDefault(context.Context) (*Project, *Response, error)
|
||||
Get(context.Context, string) (*Project, *Response, error)
|
||||
Create(context.Context, *CreateProjectRequest) (*Project, *Response, error)
|
||||
Update(context.Context, string, *UpdateProjectRequest) (*Project, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
|
||||
ListResources(context.Context, string, *ListOptions) ([]ProjectResource, *Response, error)
|
||||
AssignResources(context.Context, string, ...interface{}) ([]ProjectResource, *Response, error)
|
||||
}
|
||||
|
||||
// ProjectsServiceOp handles communication with Projects methods of the DigitalOcean API.
|
||||
type ProjectsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Project represents a DigitalOcean Project configuration.
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
OwnerUUID string `json:"owner_uuid"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Purpose string `json:"purpose"`
|
||||
Environment string `json:"environment"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a Project.
|
||||
func (p Project) String() string {
|
||||
return Stringify(p)
|
||||
}
|
||||
|
||||
// CreateProjectRequest represents the request to create a new project.
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Purpose string `json:"purpose"`
|
||||
Environment string `json:"environment"`
|
||||
}
|
||||
|
||||
// UpdateProjectRequest represents the request to update project information.
|
||||
// This type expects certain attribute types, but is built this way to allow
|
||||
// nil values as well. See `updateProjectRequest` for the "real" types.
|
||||
type UpdateProjectRequest struct {
|
||||
Name interface{}
|
||||
Description interface{}
|
||||
Purpose interface{}
|
||||
Environment interface{}
|
||||
IsDefault interface{}
|
||||
}
|
||||
|
||||
type updateProjectRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Purpose *string `json:"purpose"`
|
||||
Environment *string `json:"environment"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// MarshalJSON takes an UpdateRequest and converts it to the "typed" request
|
||||
// which is sent to the projects API. This is a PATCH request, which allows
|
||||
// partial attributes, so `null` values are OK.
|
||||
func (upr *UpdateProjectRequest) MarshalJSON() ([]byte, error) {
|
||||
d := &updateProjectRequest{}
|
||||
if str, ok := upr.Name.(string); ok {
|
||||
d.Name = &str
|
||||
}
|
||||
if str, ok := upr.Description.(string); ok {
|
||||
d.Description = &str
|
||||
}
|
||||
if str, ok := upr.Purpose.(string); ok {
|
||||
d.Purpose = &str
|
||||
}
|
||||
if str, ok := upr.Environment.(string); ok {
|
||||
d.Environment = &str
|
||||
}
|
||||
if val, ok := upr.IsDefault.(bool); ok {
|
||||
d.IsDefault = &val
|
||||
}
|
||||
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
type assignResourcesRequest struct {
|
||||
Resources []string `json:"resources"`
|
||||
}
|
||||
|
||||
// ProjectResource is the projects API's representation of a resource.
|
||||
type ProjectResource struct {
|
||||
URN string `json:"urn"`
|
||||
AssignedAt string `json:"assigned_at"`
|
||||
Links *ProjectResourceLinks `json:"links"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ProjetResourceLinks specify the link for more information about the resource.
|
||||
type ProjectResourceLinks struct {
|
||||
Self string `json:"self"`
|
||||
}
|
||||
|
||||
type projectsRoot struct {
|
||||
Projects []Project `json:"projects"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type projectRoot struct {
|
||||
Project *Project `json:"project"`
|
||||
}
|
||||
|
||||
type projectResourcesRoot struct {
|
||||
Resources []ProjectResource `json:"resources"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
var _ ProjectsService = &ProjectsServiceOp{}
|
||||
|
||||
// List Projects.
|
||||
func (p *ProjectsServiceOp) List(ctx context.Context, opts *ListOptions) ([]Project, *Response, error) {
|
||||
path, err := addOptions(projectsBasePath, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(projectsRoot)
|
||||
resp, err := p.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Projects, resp, err
|
||||
}
|
||||
|
||||
// GetDefault project.
|
||||
func (p *ProjectsServiceOp) GetDefault(ctx context.Context) (*Project, *Response, error) {
|
||||
return p.getHelper(ctx, "default")
|
||||
}
|
||||
|
||||
// Get retrieves a single project by its ID.
|
||||
func (p *ProjectsServiceOp) Get(ctx context.Context, projectID string) (*Project, *Response, error) {
|
||||
return p.getHelper(ctx, projectID)
|
||||
}
|
||||
|
||||
// Create a new project.
|
||||
func (p *ProjectsServiceOp) Create(ctx context.Context, cr *CreateProjectRequest) (*Project, *Response, error) {
|
||||
req, err := p.client.NewRequest(ctx, http.MethodPost, projectsBasePath, cr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(projectRoot)
|
||||
resp, err := p.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Project, resp, err
|
||||
}
|
||||
|
||||
// Update an existing project.
|
||||
func (p *ProjectsServiceOp) Update(ctx context.Context, projectID string, ur *UpdateProjectRequest) (*Project, *Response, error) {
|
||||
path := path.Join(projectsBasePath, projectID)
|
||||
req, err := p.client.NewRequest(ctx, http.MethodPatch, path, ur)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(projectRoot)
|
||||
resp, err := p.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Project, resp, err
|
||||
}
|
||||
|
||||
// Delete an existing project. You cannot have any resources in a project
|
||||
// before deleting it. See the API documentation for more details.
|
||||
func (p *ProjectsServiceOp) Delete(ctx context.Context, projectID string) (*Response, error) {
|
||||
path := path.Join(projectsBasePath, projectID)
|
||||
req, err := p.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// ListResources lists all resources in a project.
|
||||
func (p *ProjectsServiceOp) ListResources(ctx context.Context, projectID string, opts *ListOptions) ([]ProjectResource, *Response, error) {
|
||||
basePath := path.Join(projectsBasePath, projectID, "resources")
|
||||
path, err := addOptions(basePath, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(projectResourcesRoot)
|
||||
resp, err := p.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Resources, resp, err
|
||||
}
|
||||
|
||||
// AssignResources assigns one or more resources to a project. AssignResources
|
||||
// accepts resources in two possible formats:
|
||||
|
||||
// 1. The resource type, like `&Droplet{ID: 1}` or `&FloatingIP{IP: "1.2.3.4"}`
|
||||
// 2. A valid DO URN as a string, like "do:droplet:1234"
|
||||
//
|
||||
// There is no unassign. To move a resource to another project, just assign
|
||||
// it to that other project.
|
||||
func (p *ProjectsServiceOp) AssignResources(ctx context.Context, projectID string, resources ...interface{}) ([]ProjectResource, *Response, error) {
|
||||
path := path.Join(projectsBasePath, projectID, "resources")
|
||||
|
||||
ar := &assignResourcesRequest{
|
||||
Resources: make([]string, len(resources)),
|
||||
}
|
||||
|
||||
for i, resource := range resources {
|
||||
switch resource := resource.(type) {
|
||||
case ResourceWithURN:
|
||||
ar.Resources[i] = resource.URN()
|
||||
case string:
|
||||
ar.Resources[i] = resource
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("%T must either be a string or have a valid URN method", resource)
|
||||
}
|
||||
}
|
||||
req, err := p.client.NewRequest(ctx, http.MethodPost, path, ar)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(projectResourcesRoot)
|
||||
resp, err := p.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Resources, resp, err
|
||||
}
|
||||
|
||||
func (p *ProjectsServiceOp) getHelper(ctx context.Context, projectID string) (*Project, *Response, error) {
|
||||
path := path.Join(projectsBasePath, projectID)
|
||||
|
||||
req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(projectRoot)
|
||||
resp, err := p.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Project, resp, err
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RegionsService is an interface for interfacing with the regions
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#regions
|
||||
type RegionsService interface {
|
||||
List(context.Context, *ListOptions) ([]Region, *Response, error)
|
||||
}
|
||||
|
||||
// RegionsServiceOp handles communication with the region related methods of the
|
||||
// DigitalOcean API.
|
||||
type RegionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ RegionsService = &RegionsServiceOp{}
|
||||
|
||||
// Region represents a DigitalOcean Region
|
||||
type Region struct {
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Sizes []string `json:"sizes,omitempty"`
|
||||
Available bool `json:"available,omitempty"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
type regionsRoot struct {
|
||||
Regions []Region
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
func (r Region) String() string {
|
||||
return Stringify(r)
|
||||
}
|
||||
|
||||
// List all regions
|
||||
func (s *RegionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Region, *Response, error) {
|
||||
path := "v2/regions"
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(regionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Regions, resp, err
|
||||
}
|
@ -0,0 +1,253 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
registryPath = "/v2/registry"
|
||||
// RegistryServer is the hostname of the DigitalOcean registry service
|
||||
RegistryServer = "registry.digitalocean.com"
|
||||
)
|
||||
|
||||
// RegistryService is an interface for interfacing with the Registry endpoints
|
||||
// of the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#registry
|
||||
type RegistryService interface {
|
||||
Create(context.Context, *RegistryCreateRequest) (*Registry, *Response, error)
|
||||
Get(context.Context) (*Registry, *Response, error)
|
||||
Delete(context.Context) (*Response, error)
|
||||
DockerCredentials(context.Context, *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error)
|
||||
ListRepositories(context.Context, string, *ListOptions) ([]*Repository, *Response, error)
|
||||
ListRepositoryTags(context.Context, string, string, *ListOptions) ([]*RepositoryTag, *Response, error)
|
||||
DeleteTag(context.Context, string, string, string) (*Response, error)
|
||||
DeleteManifest(context.Context, string, string, string) (*Response, error)
|
||||
}
|
||||
|
||||
var _ RegistryService = &RegistryServiceOp{}
|
||||
|
||||
// RegistryServiceOp handles communication with Registry methods of the DigitalOcean API.
|
||||
type RegistryServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// RegistryCreateRequest represents a request to create a registry.
|
||||
type RegistryCreateRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// RegistryDockerCredentialsRequest represents a request to retrieve docker
|
||||
// credentials for a registry.
|
||||
type RegistryDockerCredentialsRequest struct {
|
||||
ReadWrite bool `json:"read_write"`
|
||||
ExpirySeconds *int `json:"expiry_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// Registry represents a registry.
|
||||
type Registry struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// Repository represents a repository
|
||||
type Repository struct {
|
||||
RegistryName string `json:"registry_name,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
LatestTag *RepositoryTag `json:"latest_tag,omitempty"`
|
||||
TagCount uint64 `json:"tag_count,omitempty"`
|
||||
}
|
||||
|
||||
// RepositoryTag represents a repository tag
|
||||
type RepositoryTag struct {
|
||||
RegistryName string `json:"registry_name,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
ManifestDigest string `json:"manifest_digest,omitempty"`
|
||||
CompressedSizeBytes uint64 `json:"compressed_size_bytes,omitempty"`
|
||||
SizeBytes uint64 `json:"size_bytes,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type registryRoot struct {
|
||||
Registry *Registry `json:"registry,omitempty"`
|
||||
}
|
||||
|
||||
type repositoriesRoot struct {
|
||||
Repositories []*Repository `json:"repositories,omitempty"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type repositoryTagsRoot struct {
|
||||
Tags []*RepositoryTag `json:"tags,omitempty"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// Get retrieves the details of a Registry.
|
||||
func (svc *RegistryServiceOp) Get(ctx context.Context) (*Registry, *Response, error) {
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, registryPath, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(registryRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Registry, resp, nil
|
||||
}
|
||||
|
||||
// Create creates a registry.
|
||||
func (svc *RegistryServiceOp) Create(ctx context.Context, create *RegistryCreateRequest) (*Registry, *Response, error) {
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, registryPath, create)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(registryRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Registry, resp, nil
|
||||
}
|
||||
|
||||
// Delete deletes a registry. There is no way to recover a registry once it has
|
||||
// been destroyed.
|
||||
func (svc *RegistryServiceOp) Delete(ctx context.Context) (*Response, error) {
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, registryPath, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DockerCredentials is the content of a Docker config file
|
||||
// that is used by the docker CLI
|
||||
// See: https://docs.docker.com/engine/reference/commandline/cli/#configjson-properties
|
||||
type DockerCredentials struct {
|
||||
DockerConfigJSON []byte
|
||||
}
|
||||
|
||||
// DockerCredentials retrieves a Docker config file containing the registry's credentials.
|
||||
func (svc *RegistryServiceOp) DockerCredentials(ctx context.Context, request *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", registryPath, "docker-credentials")
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("read_write", strconv.FormatBool(request.ReadWrite))
|
||||
if request.ExpirySeconds != nil {
|
||||
q.Add("expiry_seconds", strconv.Itoa(*request.ExpirySeconds))
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
var buf bytes.Buffer
|
||||
resp, err := svc.client.Do(ctx, req, &buf)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
dc := &DockerCredentials{
|
||||
DockerConfigJSON: buf.Bytes(),
|
||||
}
|
||||
return dc, resp, nil
|
||||
}
|
||||
|
||||
// ListRepositories returns a list of the Repositories visible with the registry's credentials.
|
||||
func (svc *RegistryServiceOp) ListRepositories(ctx context.Context, registry string, opts *ListOptions) ([]*Repository, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/repositories", registryPath, registry)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(repositoriesRoot)
|
||||
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Repositories, resp, nil
|
||||
}
|
||||
|
||||
// ListRepositoryTags returns a list of the RepositoryTags available within the given repository.
|
||||
func (svc *RegistryServiceOp) ListRepositoryTags(ctx context.Context, registry, repository string, opts *ListOptions) ([]*RepositoryTag, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/repositories/%s/tags", registryPath, registry, url.PathEscape(repository))
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
root := new(repositoryTagsRoot)
|
||||
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Tags, resp, nil
|
||||
}
|
||||
|
||||
// DeleteTag deletes a tag within a given repository.
|
||||
func (svc *RegistryServiceOp) DeleteTag(ctx context.Context, registry, repository, tag string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/repositories/%s/tags/%s", registryPath, registry, url.PathEscape(repository), tag)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteManifest deletes a manifest by its digest within a given repository.
|
||||
func (svc *RegistryServiceOp) DeleteManifest(ctx context.Context, registry, repository, digest string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/repositories/%s/digests/%s", registryPath, registry, url.PathEscape(repository), digest)
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := svc.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SizesService is an interface for interfacing with the size
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#sizes
|
||||
type SizesService interface {
|
||||
List(context.Context, *ListOptions) ([]Size, *Response, error)
|
||||
}
|
||||
|
||||
// SizesServiceOp handles communication with the size related methods of the
|
||||
// DigitalOcean API.
|
||||
type SizesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ SizesService = &SizesServiceOp{}
|
||||
|
||||
// Size represents a DigitalOcean Size
|
||||
type Size struct {
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Memory int `json:"memory,omitempty"`
|
||||
Vcpus int `json:"vcpus,omitempty"`
|
||||
Disk int `json:"disk,omitempty"`
|
||||
PriceMonthly float64 `json:"price_monthly,omitempty"`
|
||||
PriceHourly float64 `json:"price_hourly,omitempty"`
|
||||
Regions []string `json:"regions,omitempty"`
|
||||
Available bool `json:"available,omitempty"`
|
||||
Transfer float64 `json:"transfer,omitempty"`
|
||||
}
|
||||
|
||||
func (s Size) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
type sizesRoot struct {
|
||||
Sizes []Size
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// List all images
|
||||
func (s *SizesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Size, *Response, error) {
|
||||
path := "v2/sizes"
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(sizesRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Sizes, resp, err
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const snapshotBasePath = "v2/snapshots"
|
||||
|
||||
// SnapshotsService is an interface for interfacing with the snapshots
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#snapshots
|
||||
type SnapshotsService interface {
|
||||
List(context.Context, *ListOptions) ([]Snapshot, *Response, error)
|
||||
ListVolume(context.Context, *ListOptions) ([]Snapshot, *Response, error)
|
||||
ListDroplet(context.Context, *ListOptions) ([]Snapshot, *Response, error)
|
||||
Get(context.Context, string) (*Snapshot, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// SnapshotsServiceOp handles communication with the snapshot related methods of the
|
||||
// DigitalOcean API.
|
||||
type SnapshotsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ SnapshotsService = &SnapshotsServiceOp{}
|
||||
|
||||
// Snapshot represents a DigitalOcean Snapshot
|
||||
type Snapshot struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ResourceType string `json:"resource_type,omitempty"`
|
||||
Regions []string `json:"regions,omitempty"`
|
||||
MinDiskSize int `json:"min_disk_size,omitempty"`
|
||||
SizeGigaBytes float64 `json:"size_gigabytes,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type snapshotRoot struct {
|
||||
Snapshot *Snapshot `json:"snapshot"`
|
||||
}
|
||||
|
||||
type snapshotsRoot struct {
|
||||
Snapshots []Snapshot `json:"snapshots"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type listSnapshotOptions struct {
|
||||
ResourceType string `url:"resource_type,omitempty"`
|
||||
}
|
||||
|
||||
func (s Snapshot) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
// List lists all the snapshots available.
|
||||
func (s *SnapshotsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
return s.list(ctx, opt, nil)
|
||||
}
|
||||
|
||||
// ListDroplet lists all the Droplet snapshots.
|
||||
func (s *SnapshotsServiceOp) ListDroplet(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
listOpt := listSnapshotOptions{ResourceType: "droplet"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// ListVolume lists all the volume snapshots.
|
||||
func (s *SnapshotsServiceOp) ListVolume(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
listOpt := listSnapshotOptions{ResourceType: "volume"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// Get retrieves an snapshot by id.
|
||||
func (s *SnapshotsServiceOp) Get(ctx context.Context, snapshotID string) (*Snapshot, *Response, error) {
|
||||
return s.get(ctx, snapshotID)
|
||||
}
|
||||
|
||||
// Delete an snapshot.
|
||||
func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", snapshotBasePath, snapshotID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Helper method for getting an individual snapshot
|
||||
func (s *SnapshotsServiceOp) get(ctx context.Context, ID string) (*Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", snapshotBasePath, ID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Snapshot, resp, err
|
||||
}
|
||||
|
||||
// Helper method for listing snapshots
|
||||
func (s *SnapshotsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listSnapshotOptions) ([]Snapshot, *Response, error) {
|
||||
path := snapshotBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
path, err = addOptions(path, listOpt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Snapshots, resp, err
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
storageBasePath = "v2"
|
||||
storageAllocPath = storageBasePath + "/volumes"
|
||||
storageSnapPath = storageBasePath + "/snapshots"
|
||||
)
|
||||
|
||||
// StorageService is an interface for interfacing with the storage
|
||||
// endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#block-storage
|
||||
type StorageService interface {
|
||||
ListVolumes(context.Context, *ListVolumeParams) ([]Volume, *Response, error)
|
||||
GetVolume(context.Context, string) (*Volume, *Response, error)
|
||||
CreateVolume(context.Context, *VolumeCreateRequest) (*Volume, *Response, error)
|
||||
DeleteVolume(context.Context, string) (*Response, error)
|
||||
ListSnapshots(ctx context.Context, volumeID string, opts *ListOptions) ([]Snapshot, *Response, error)
|
||||
GetSnapshot(context.Context, string) (*Snapshot, *Response, error)
|
||||
CreateSnapshot(context.Context, *SnapshotCreateRequest) (*Snapshot, *Response, error)
|
||||
DeleteSnapshot(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// StorageServiceOp handles communication with the storage volumes related methods of the
|
||||
// DigitalOcean API.
|
||||
type StorageServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ListVolumeParams stores the options you can set for a ListVolumeCall
|
||||
type ListVolumeParams struct {
|
||||
Region string `json:"region"`
|
||||
Name string `json:"name"`
|
||||
ListOptions *ListOptions `json:"list_options,omitempty"`
|
||||
}
|
||||
|
||||
var _ StorageService = &StorageServiceOp{}
|
||||
|
||||
// Volume represents a Digital Ocean block store volume.
|
||||
type Volume struct {
|
||||
ID string `json:"id"`
|
||||
Region *Region `json:"region"`
|
||||
Name string `json:"name"`
|
||||
SizeGigaBytes int64 `json:"size_gigabytes"`
|
||||
Description string `json:"description"`
|
||||
DropletIDs []int `json:"droplet_ids"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
FilesystemType string `json:"filesystem_type"`
|
||||
FilesystemLabel string `json:"filesystem_label"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (f Volume) String() string {
|
||||
return Stringify(f)
|
||||
}
|
||||
|
||||
func (f Volume) URN() string {
|
||||
return ToURN("Volume", f.ID)
|
||||
}
|
||||
|
||||
type storageVolumesRoot struct {
|
||||
Volumes []Volume `json:"volumes"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type storageVolumeRoot struct {
|
||||
Volume *Volume `json:"volume"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// VolumeCreateRequest represents a request to create a block store
|
||||
// volume.
|
||||
type VolumeCreateRequest struct {
|
||||
Region string `json:"region"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
SizeGigaBytes int64 `json:"size_gigabytes"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
FilesystemType string `json:"filesystem_type"`
|
||||
FilesystemLabel string `json:"filesystem_label"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// ListVolumes lists all storage volumes.
|
||||
func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolumeParams) ([]Volume, *Response, error) {
|
||||
path := storageAllocPath
|
||||
if params != nil {
|
||||
if params.Region != "" && params.Name != "" {
|
||||
path = fmt.Sprintf("%s?name=%s®ion=%s", path, params.Name, params.Region)
|
||||
} else if params.Region != "" {
|
||||
path = fmt.Sprintf("%s?region=%s", path, params.Region)
|
||||
} else if params.Name != "" {
|
||||
path = fmt.Sprintf("%s?name=%s", path, params.Name)
|
||||
}
|
||||
|
||||
if params.ListOptions != nil {
|
||||
var err error
|
||||
path, err = addOptions(path, params.ListOptions)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(storageVolumesRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Volumes, resp, nil
|
||||
}
|
||||
|
||||
// CreateVolume creates a storage volume. The name must be unique.
|
||||
func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *VolumeCreateRequest) (*Volume, *Response, error) {
|
||||
path := storageAllocPath
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(storageVolumeRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Volume, resp, nil
|
||||
}
|
||||
|
||||
// GetVolume retrieves an individual storage volume.
|
||||
func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(storageVolumeRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Volume, resp, nil
|
||||
}
|
||||
|
||||
// DeleteVolume deletes a storage volume.
|
||||
func (svc *StorageServiceOp) DeleteVolume(ctx context.Context, id string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// SnapshotCreateRequest represents a request to create a block store
|
||||
// volume.
|
||||
type SnapshotCreateRequest struct {
|
||||
VolumeID string `json:"volume_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// ListSnapshots lists all snapshots related to a storage volume.
|
||||
func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, volumeID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotsRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Snapshots, resp, nil
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a snapshot of a storage volume.
|
||||
func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.VolumeID)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Snapshot, resp, nil
|
||||
}
|
||||
|
||||
// GetSnapshot retrieves an individual snapshot.
|
||||
func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Snapshot, resp, nil
|
||||
}
|
||||
|
||||
// DeleteSnapshot deletes a snapshot.
|
||||
func (svc *StorageServiceOp) DeleteSnapshot(ctx context.Context, id string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.client.Do(ctx, req, nil)
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// StorageActionsService is an interface for interfacing with the
|
||||
// storage actions endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#storage-actions
|
||||
type StorageActionsService interface {
|
||||
Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
|
||||
DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
|
||||
Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error)
|
||||
List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error)
|
||||
Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// StorageActionsServiceOp handles communication with the storage volumes
|
||||
// action related methods of the DigitalOcean API.
|
||||
type StorageActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// StorageAttachment represents the attachement of a block storage
|
||||
// volume to a specific Droplet under the device name.
|
||||
type StorageAttachment struct {
|
||||
DropletID int `json:"droplet_id"`
|
||||
}
|
||||
|
||||
// Attach a storage volume to a Droplet.
|
||||
func (s *StorageActionsServiceOp) Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "attach",
|
||||
"droplet_id": dropletID,
|
||||
}
|
||||
return s.doAction(ctx, volumeID, request)
|
||||
}
|
||||
|
||||
// DetachByDropletID a storage volume from a Droplet by Droplet ID.
|
||||
func (s *StorageActionsServiceOp) DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "detach",
|
||||
"droplet_id": dropletID,
|
||||
}
|
||||
return s.doAction(ctx, volumeID, request)
|
||||
}
|
||||
|
||||
// Get an action for a particular storage volume by id.
|
||||
func (s *StorageActionsServiceOp) Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%d", storageAllocationActionPath(volumeID), actionID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// List the actions for a particular storage volume.
|
||||
func (s *StorageActionsServiceOp) List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error) {
|
||||
path := storageAllocationActionPath(volumeID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
// Resize a storage volume.
|
||||
func (s *StorageActionsServiceOp) Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "resize",
|
||||
"size_gigabytes": sizeGigabytes,
|
||||
"region": regionSlug,
|
||||
}
|
||||
return s.doAction(ctx, volumeID, request)
|
||||
}
|
||||
|
||||
func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string, request *ActionRequest) (*Action, *Response, error) {
|
||||
path := storageAllocationActionPath(volumeID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *StorageActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
func storageAllocationActionPath(volumeID string) string {
|
||||
return fmt.Sprintf("%s/%s/actions", storageAllocPath, volumeID)
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var timestampType = reflect.TypeOf(Timestamp{})
|
||||
|
||||
type ResourceWithURN interface {
|
||||
URN() string
|
||||
}
|
||||
|
||||
// ToURN converts the resource type and ID to a valid DO API URN.
|
||||
func ToURN(resourceType string, id interface{}) string {
|
||||
return fmt.Sprintf("%s:%s:%v", "do", strings.ToLower(resourceType), id)
|
||||
}
|
||||
|
||||
// Stringify attempts to create a string representation of DigitalOcean types
|
||||
func Stringify(message interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
v := reflect.ValueOf(message)
|
||||
stringifyValue(&buf, v)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// stringifyValue was graciously cargoculted from the goprotubuf library
|
||||
func stringifyValue(w io.Writer, val reflect.Value) {
|
||||
if val.Kind() == reflect.Ptr && val.IsNil() {
|
||||
_, _ = w.Write([]byte("<nil>"))
|
||||
return
|
||||
}
|
||||
|
||||
v := reflect.Indirect(val)
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
fmt.Fprintf(w, `"%s"`, v)
|
||||
case reflect.Slice:
|
||||
stringifySlice(w, v)
|
||||
return
|
||||
case reflect.Struct:
|
||||
stringifyStruct(w, v)
|
||||
default:
|
||||
if v.CanInterface() {
|
||||
fmt.Fprint(w, v.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stringifySlice(w io.Writer, v reflect.Value) {
|
||||
_, _ = w.Write([]byte{'['})
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if i > 0 {
|
||||
_, _ = w.Write([]byte{' '})
|
||||
}
|
||||
|
||||
stringifyValue(w, v.Index(i))
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte{']'})
|
||||
}
|
||||
|
||||
func stringifyStruct(w io.Writer, v reflect.Value) {
|
||||
if v.Type().Name() != "" {
|
||||
_, _ = w.Write([]byte(v.Type().String()))
|
||||
}
|
||||
|
||||
// special handling of Timestamp values
|
||||
if v.Type() == timestampType {
|
||||
fmt.Fprintf(w, "{%s}", v.Interface())
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte{'{'})
|
||||
|
||||
var sep bool
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fv := v.Field(i)
|
||||
if fv.Kind() == reflect.Ptr && fv.IsNil() {
|
||||
continue
|
||||
}
|
||||
if fv.Kind() == reflect.Slice && fv.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
if sep {
|
||||
_, _ = w.Write([]byte(", "))
|
||||
} else {
|
||||
sep = true
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(v.Type().Field(i).Name))
|
||||
_, _ = w.Write([]byte{':'})
|
||||
stringifyValue(w, fv)
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte{'}'})
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const tagsBasePath = "v2/tags"
|
||||
|
||||
// TagsService is an interface for interfacing with the tags
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#tags
|
||||
type TagsService interface {
|
||||
List(context.Context, *ListOptions) ([]Tag, *Response, error)
|
||||
Get(context.Context, string) (*Tag, *Response, error)
|
||||
Create(context.Context, *TagCreateRequest) (*Tag, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
|
||||
TagResources(context.Context, string, *TagResourcesRequest) (*Response, error)
|
||||
UntagResources(context.Context, string, *UntagResourcesRequest) (*Response, error)
|
||||
}
|
||||
|
||||
// TagsServiceOp handles communication with tag related method of the
|
||||
// DigitalOcean API.
|
||||
type TagsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ TagsService = &TagsServiceOp{}
|
||||
|
||||
// ResourceType represents a class of resource, currently only droplet are supported
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
// DropletResourceType holds the string representing our ResourceType of Droplet.
|
||||
DropletResourceType ResourceType = "droplet"
|
||||
// ImageResourceType holds the string representing our ResourceType of Image.
|
||||
ImageResourceType ResourceType = "image"
|
||||
// VolumeResourceType holds the string representing our ResourceType of Volume.
|
||||
VolumeResourceType ResourceType = "volume"
|
||||
// LoadBalancerResourceType holds the string representing our ResourceType of LoadBalancer.
|
||||
LoadBalancerResourceType ResourceType = "load_balancer"
|
||||
// VolumeSnapshotResourceType holds the string representing our ResourceType for storage Snapshots.
|
||||
VolumeSnapshotResourceType ResourceType = "volume_snapshot"
|
||||
// DatabaseResourceType holds the string representing our ResourceType of Database.
|
||||
DatabaseResourceType ResourceType = "database"
|
||||
)
|
||||
|
||||
// Resource represent a single resource for associating/disassociating with tags
|
||||
type Resource struct {
|
||||
ID string `json:"resource_id,omitempty"`
|
||||
Type ResourceType `json:"resource_type,omitempty"`
|
||||
}
|
||||
|
||||
// TaggedResources represent the set of resources a tag is attached to
|
||||
type TaggedResources struct {
|
||||
Count int `json:"count"`
|
||||
LastTaggedURI string `json:"last_tagged_uri,omitempty"`
|
||||
Droplets *TaggedDropletsResources `json:"droplets,omitempty"`
|
||||
Images *TaggedImagesResources `json:"images"`
|
||||
Volumes *TaggedVolumesResources `json:"volumes"`
|
||||
VolumeSnapshots *TaggedVolumeSnapshotsResources `json:"volume_snapshots"`
|
||||
Databases *TaggedDatabasesResources `json:"databases"`
|
||||
}
|
||||
|
||||
// TaggedDropletsResources represent the droplet resources a tag is attached to
|
||||
type TaggedDropletsResources struct {
|
||||
Count int `json:"count,float64,omitempty"`
|
||||
LastTagged *Droplet `json:"last_tagged,omitempty"`
|
||||
LastTaggedURI string `json:"last_tagged_uri,omitempty"`
|
||||
}
|
||||
|
||||
// TaggedResourcesData represent the generic resources a tag is attached to
|
||||
type TaggedResourcesData struct {
|
||||
Count int `json:"count,float64,omitempty"`
|
||||
LastTaggedURI string `json:"last_tagged_uri,omitempty"`
|
||||
}
|
||||
|
||||
// TaggedImagesResources represent the image resources a tag is attached to
|
||||
type TaggedImagesResources TaggedResourcesData
|
||||
|
||||
// TaggedVolumesResources represent the volume resources a tag is attached to
|
||||
type TaggedVolumesResources TaggedResourcesData
|
||||
|
||||
// TaggedVolumeSnapshotsResources represent the volume snapshot resources a tag is attached to
|
||||
type TaggedVolumeSnapshotsResources TaggedResourcesData
|
||||
|
||||
// TaggedDatabasesResources represent the database resources a tag is attached to
|
||||
type TaggedDatabasesResources TaggedResourcesData
|
||||
|
||||
// Tag represent DigitalOcean tag
|
||||
type Tag struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Resources *TaggedResources `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
//TagCreateRequest represents the JSON structure of a request of that type.
|
||||
type TagCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TagResourcesRequest represents the JSON structure of a request of that type.
|
||||
type TagResourcesRequest struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
// UntagResourcesRequest represents the JSON structure of a request of that type.
|
||||
type UntagResourcesRequest struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
type tagsRoot struct {
|
||||
Tags []Tag `json:"tags"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
type tagRoot struct {
|
||||
Tag *Tag `json:"tag"`
|
||||
}
|
||||
|
||||
// List all tags
|
||||
func (s *TagsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Tag, *Response, error) {
|
||||
path := tagsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(tagsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.Tags, resp, err
|
||||
}
|
||||
|
||||
// Get a single tag
|
||||
func (s *TagsServiceOp) Get(ctx context.Context, name string) (*Tag, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(tagRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Tag, resp, err
|
||||
}
|
||||
|
||||
// Create a new tag
|
||||
func (s *TagsServiceOp) Create(ctx context.Context, createRequest *TagCreateRequest) (*Tag, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, tagsBasePath, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(tagRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Tag, resp, err
|
||||
}
|
||||
|
||||
// Delete an existing tag
|
||||
func (s *TagsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
|
||||
if name == "" {
|
||||
return nil, NewArgError("name", "cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// TagResources associates resources with a given Tag.
|
||||
func (s *TagsServiceOp) TagResources(ctx context.Context, name string, tagRequest *TagResourcesRequest) (*Response, error) {
|
||||
if name == "" {
|
||||
return nil, NewArgError("name", "cannot be empty")
|
||||
}
|
||||
|
||||
if tagRequest == nil {
|
||||
return nil, NewArgError("tagRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, tagRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// UntagResources dissociates resources with a given Tag.
|
||||
func (s *TagsServiceOp) UntagResources(ctx context.Context, name string, untagRequest *UntagResourcesRequest) (*Response, error) {
|
||||
if name == "" {
|
||||
return nil, NewArgError("name", "cannot be empty")
|
||||
}
|
||||
|
||||
if untagRequest == nil {
|
||||
return nil, NewArgError("tagRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, untagRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Timestamp represents a time that can be unmarshalled from a JSON string
|
||||
// formatted as either an RFC3339 or Unix timestamp. All
|
||||
// exported methods of time.Time can be called on Timestamp.
|
||||
type Timestamp struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (t Timestamp) String() string {
|
||||
return t.Time.String()
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
// Time is expected in RFC3339 or Unix format.
|
||||
func (t *Timestamp) UnmarshalJSON(data []byte) error {
|
||||
str := string(data)
|
||||
i, err := strconv.ParseInt(str, 10, 64)
|
||||
if err == nil {
|
||||
t.Time = time.Unix(i, 0)
|
||||
} else {
|
||||
t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Equal reports whether t and u are equal based on time.Equal
|
||||
func (t Timestamp) Equal(u Timestamp) bool {
|
||||
return t.Time.Equal(u.Time)
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
package godo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const vpcsBasePath = "/v2/vpcs"
|
||||
|
||||
// VPCsService is an interface for managing Virtual Private Cloud configurations with the
|
||||
// DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#vpcs
|
||||
type VPCsService interface {
|
||||
Create(context.Context, *VPCCreateRequest) (*VPC, *Response, error)
|
||||
Get(context.Context, string) (*VPC, *Response, error)
|
||||
List(context.Context, *ListOptions) ([]*VPC, *Response, error)
|
||||
Update(context.Context, string, *VPCUpdateRequest) (*VPC, *Response, error)
|
||||
Set(context.Context, string, ...VPCSetField) (*VPC, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
var _ VPCsService = &VPCsServiceOp{}
|
||||
|
||||
// VPCsServiceOp interfaces with VPC endpoints in the DigitalOcean API.
|
||||
type VPCsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// VPCCreateRequest represents a request to create a Virtual Private Cloud.
|
||||
type VPCCreateRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
RegionSlug string `json:"region,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IPRange string `json:"ip_range,omitempty"`
|
||||
}
|
||||
|
||||
// VPCUpdateRequest represents a request to update a Virtual Private Cloud.
|
||||
type VPCUpdateRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// VPCSetField allows one to set individual fields within a VPC configuration.
|
||||
type VPCSetField interface {
|
||||
vpcSetField(map[string]interface{})
|
||||
}
|
||||
|
||||
// VPCSetName is used when one want to set the `name` field of a VPC.
|
||||
// Ex.: VPCs.Set(..., VPCSetName("new-name"))
|
||||
type VPCSetName string
|
||||
|
||||
// VPCSetDescription is used when one want to set the `description` field of a VPC.
|
||||
// Ex.: VPCs.Set(..., VPCSetDescription("vpc description"))
|
||||
type VPCSetDescription string
|
||||
|
||||
// VPC represents a DigitalOcean Virtual Private Cloud configuration.
|
||||
type VPC struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
URN string `json:"urn"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IPRange string `json:"ip_range,omitempty"`
|
||||
RegionSlug string `json:"region,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
Default bool `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
type vpcRoot struct {
|
||||
VPC *VPC `json:"vpc"`
|
||||
}
|
||||
|
||||
type vpcsRoot struct {
|
||||
VPCs []*VPC `json:"vpcs"`
|
||||
Links *Links `json:"links"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// Get returns the details of a Virtual Private Cloud.
|
||||
func (v *VPCsServiceOp) Get(ctx context.Context, id string) (*VPC, *Response, error) {
|
||||
path := vpcsBasePath + "/" + id
|
||||
req, err := v.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(vpcRoot)
|
||||
resp, err := v.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.VPC, resp, nil
|
||||
}
|
||||
|
||||
// Create creates a new Virtual Private Cloud.
|
||||
func (v *VPCsServiceOp) Create(ctx context.Context, create *VPCCreateRequest) (*VPC, *Response, error) {
|
||||
path := vpcsBasePath
|
||||
req, err := v.client.NewRequest(ctx, http.MethodPost, path, create)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(vpcRoot)
|
||||
resp, err := v.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.VPC, resp, nil
|
||||
}
|
||||
|
||||
// List returns a list of the caller's VPCs, with optional pagination.
|
||||
func (v *VPCsServiceOp) List(ctx context.Context, opt *ListOptions) ([]*VPC, *Response, error) {
|
||||
path, err := addOptions(vpcsBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req, err := v.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(vpcsRoot)
|
||||
resp, err := v.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
if m := root.Meta; m != nil {
|
||||
resp.Meta = m
|
||||
}
|
||||
|
||||
return root.VPCs, resp, nil
|
||||
}
|
||||
|
||||
// Update updates a Virtual Private Cloud's properties.
|
||||
func (v *VPCsServiceOp) Update(ctx context.Context, id string, update *VPCUpdateRequest) (*VPC, *Response, error) {
|
||||
path := vpcsBasePath + "/" + id
|
||||
req, err := v.client.NewRequest(ctx, http.MethodPut, path, update)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(vpcRoot)
|
||||
resp, err := v.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.VPC, resp, nil
|
||||
}
|
||||
|
||||
func (n VPCSetName) vpcSetField(in map[string]interface{}) {
|
||||
in["name"] = n
|
||||
}
|
||||
|
||||
func (n VPCSetDescription) vpcSetField(in map[string]interface{}) {
|
||||
in["description"] = n
|
||||
}
|
||||
|
||||
// Set updates specific properties of a Virtual Private Cloud.
|
||||
func (v *VPCsServiceOp) Set(ctx context.Context, id string, fields ...VPCSetField) (*VPC, *Response, error) {
|
||||
path := vpcsBasePath + "/" + id
|
||||
update := make(map[string]interface{}, len(fields))
|
||||
for _, field := range fields {
|
||||
field.vpcSetField(update)
|
||||
}
|
||||
|
||||
req, err := v.client.NewRequest(ctx, http.MethodPatch, path, update)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(vpcRoot)
|
||||
resp, err := v.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.VPC, resp, nil
|
||||
}
|
||||
|
||||
// Delete deletes a Virtual Private Cloud. There is no way to recover a VPC once it has been
|
||||
// destroyed.
|
||||
func (v *VPCsServiceOp) Delete(ctx context.Context, id string) (*Response, error) {
|
||||
path := vpcsBasePath + "/" + id
|
||||
req, err := v.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := v.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2013 Google. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,320 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package query implements encoding of structs into URL query parameters.
|
||||
//
|
||||
// As a simple example:
|
||||
//
|
||||
// type Options struct {
|
||||
// Query string `url:"q"`
|
||||
// ShowAll bool `url:"all"`
|
||||
// Page int `url:"page"`
|
||||
// }
|
||||
//
|
||||
// opt := Options{ "foo", true, 2 }
|
||||
// v, _ := query.Values(opt)
|
||||
// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
|
||||
//
|
||||
// The exact mapping between Go values and url.Values is described in the
|
||||
// documentation for the Values() function.
|
||||
package query
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var timeType = reflect.TypeOf(time.Time{})
|
||||
|
||||
var encoderType = reflect.TypeOf(new(Encoder)).Elem()
|
||||
|
||||
// Encoder is an interface implemented by any type that wishes to encode
|
||||
// itself into URL values in a non-standard way.
|
||||
type Encoder interface {
|
||||
EncodeValues(key string, v *url.Values) error
|
||||
}
|
||||
|
||||
// Values returns the url.Values encoding of v.
|
||||
//
|
||||
// Values expects to be passed a struct, and traverses it recursively using the
|
||||
// following encoding rules.
|
||||
//
|
||||
// Each exported struct field is encoded as a URL parameter unless
|
||||
//
|
||||
// - the field's tag is "-", or
|
||||
// - the field is empty and its tag specifies the "omitempty" option
|
||||
//
|
||||
// The empty values are false, 0, any nil pointer or interface value, any array
|
||||
// slice, map, or string of length zero, and any time.Time that returns true
|
||||
// for IsZero().
|
||||
//
|
||||
// The URL parameter name defaults to the struct field name but can be
|
||||
// specified in the struct field's tag value. The "url" key in the struct
|
||||
// field's tag value is the key name, followed by an optional comma and
|
||||
// options. For example:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field int `url:"-"`
|
||||
//
|
||||
// // Field appears as URL parameter "myName".
|
||||
// Field int `url:"myName"`
|
||||
//
|
||||
// // Field appears as URL parameter "myName" and the field is omitted if
|
||||
// // its value is empty
|
||||
// Field int `url:"myName,omitempty"`
|
||||
//
|
||||
// // Field appears as URL parameter "Field" (the default), but the field
|
||||
// // is skipped if empty. Note the leading comma.
|
||||
// Field int `url:",omitempty"`
|
||||
//
|
||||
// For encoding individual field values, the following type-dependent rules
|
||||
// apply:
|
||||
//
|
||||
// Boolean values default to encoding as the strings "true" or "false".
|
||||
// Including the "int" option signals that the field should be encoded as the
|
||||
// strings "1" or "0".
|
||||
//
|
||||
// time.Time values default to encoding as RFC3339 timestamps. Including the
|
||||
// "unix" option signals that the field should be encoded as a Unix time (see
|
||||
// time.Unix())
|
||||
//
|
||||
// Slice and Array values default to encoding as multiple URL values of the
|
||||
// same name. Including the "comma" option signals that the field should be
|
||||
// encoded as a single comma-delimited value. Including the "space" option
|
||||
// similarly encodes the value as a single space-delimited string. Including
|
||||
// the "semicolon" option will encode the value as a semicolon-delimited string.
|
||||
// Including the "brackets" option signals that the multiple URL values should
|
||||
// have "[]" appended to the value name. "numbered" will append a number to
|
||||
// the end of each incidence of the value name, example:
|
||||
// name0=value0&name1=value1, etc.
|
||||
//
|
||||
// Anonymous struct fields are usually encoded as if their inner exported
|
||||
// fields were fields in the outer struct, subject to the standard Go
|
||||
// visibility rules. An anonymous struct field with a name given in its URL
|
||||
// tag is treated as having that name, rather than being anonymous.
|
||||
//
|
||||
// Non-nil pointer values are encoded as the value pointed to.
|
||||
//
|
||||
// Nested structs are encoded including parent fields in value names for
|
||||
// scoping. e.g:
|
||||
//
|
||||
// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO"
|
||||
//
|
||||
// All other values are encoded using their default string representation.
|
||||
//
|
||||
// Multiple fields that encode to the same URL parameter name will be included
|
||||
// as multiple URL values of the same name.
|
||||
func Values(v interface{}) (url.Values, error) {
|
||||
values := make(url.Values)
|
||||
val := reflect.ValueOf(v)
|
||||
for val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return values, nil
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
|
||||
}
|
||||
|
||||
err := reflectValue(values, val, "")
|
||||
return values, err
|
||||
}
|
||||
|
||||
// reflectValue populates the values parameter from the struct fields in val.
|
||||
// Embedded structs are followed recursively (using the rules defined in the
|
||||
// Values function documentation) breadth-first.
|
||||
func reflectValue(values url.Values, val reflect.Value, scope string) error {
|
||||
var embedded []reflect.Value
|
||||
|
||||
typ := val.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
sf := typ.Field(i)
|
||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||
continue
|
||||
}
|
||||
|
||||
sv := val.Field(i)
|
||||
tag := sf.Tag.Get("url")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
name, opts := parseTag(tag)
|
||||
if name == "" {
|
||||
if sf.Anonymous && sv.Kind() == reflect.Struct {
|
||||
// save embedded struct for later processing
|
||||
embedded = append(embedded, sv)
|
||||
continue
|
||||
}
|
||||
|
||||
name = sf.Name
|
||||
}
|
||||
|
||||
if scope != "" {
|
||||
name = scope + "[" + name + "]"
|
||||
}
|
||||
|
||||
if opts.Contains("omitempty") && isEmptyValue(sv) {
|
||||
continue
|
||||
}
|
||||
|
||||
if sv.Type().Implements(encoderType) {
|
||||
if !reflect.Indirect(sv).IsValid() {
|
||||
sv = reflect.New(sv.Type().Elem())
|
||||
}
|
||||
|
||||
m := sv.Interface().(Encoder)
|
||||
if err := m.EncodeValues(name, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
|
||||
var del byte
|
||||
if opts.Contains("comma") {
|
||||
del = ','
|
||||
} else if opts.Contains("space") {
|
||||
del = ' '
|
||||
} else if opts.Contains("semicolon") {
|
||||
del = ';'
|
||||
} else if opts.Contains("brackets") {
|
||||
name = name + "[]"
|
||||
}
|
||||
|
||||
if del != 0 {
|
||||
s := new(bytes.Buffer)
|
||||
first := true
|
||||
for i := 0; i < sv.Len(); i++ {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
s.WriteByte(del)
|
||||
}
|
||||
s.WriteString(valueString(sv.Index(i), opts))
|
||||
}
|
||||
values.Add(name, s.String())
|
||||
} else {
|
||||
for i := 0; i < sv.Len(); i++ {
|
||||
k := name
|
||||
if opts.Contains("numbered") {
|
||||
k = fmt.Sprintf("%s%d", name, i)
|
||||
}
|
||||
values.Add(k, valueString(sv.Index(i), opts))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for sv.Kind() == reflect.Ptr {
|
||||
if sv.IsNil() {
|
||||
break
|
||||
}
|
||||
sv = sv.Elem()
|
||||
}
|
||||
|
||||
if sv.Type() == timeType {
|
||||
values.Add(name, valueString(sv, opts))
|
||||
continue
|
||||
}
|
||||
|
||||
if sv.Kind() == reflect.Struct {
|
||||
reflectValue(values, sv, name)
|
||||
continue
|
||||
}
|
||||
|
||||
values.Add(name, valueString(sv, opts))
|
||||
}
|
||||
|
||||
for _, f := range embedded {
|
||||
if err := reflectValue(values, f, scope); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// valueString returns the string representation of a value.
|
||||
func valueString(v reflect.Value, opts tagOptions) string {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return ""
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Bool && opts.Contains("int") {
|
||||
if v.Bool() {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
if v.Type() == timeType {
|
||||
t := v.Interface().(time.Time)
|
||||
if opts.Contains("unix") {
|
||||
return strconv.FormatInt(t.Unix(), 10)
|
||||
}
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return fmt.Sprint(v.Interface())
|
||||
}
|
||||
|
||||
// isEmptyValue checks if a value should be considered empty for the purposes
|
||||
// of omitting fields with the "omitempty" option.
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
return v.Len() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
return v.IsNil()
|
||||
}
|
||||
|
||||
if v.Type() == timeType {
|
||||
return v.Interface().(time.Time).IsZero()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// tagOptions is the string following a comma in a struct field's "url" tag, or
|
||||
// the empty string. It does not include the leading comma.
|
||||
type tagOptions []string
|
||||
|
||||
// parseTag splits a struct field's url tag into its name and comma-separated
|
||||
// options.
|
||||
func parseTag(tag string) (string, tagOptions) {
|
||||
s := strings.Split(tag, ",")
|
||||
return s[0], s[1:]
|
||||
}
|
||||
|
||||
// Contains checks whether the tagOptions contains the specified option.
|
||||
func (o tagOptions) Contains(option string) bool {
|
||||
for _, s := range o {
|
||||
if s == option {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Loading…
Reference in new issue