Add hetzner service discovery (#7822)

Signed-off-by: Lukas Kämmerling <lukas.kaemmerling@hetzner-cloud.de>
This commit is contained in:
Lukas Kämmerling 2020-08-21 15:49:19 +02:00 committed by GitHub
parent a1601274ba
commit b6955bf1ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 10240 additions and 187 deletions

View file

@ -35,6 +35,7 @@ import (
"github.com/prometheus/prometheus/discovery/dockerswarm"
"github.com/prometheus/prometheus/discovery/ec2"
"github.com/prometheus/prometheus/discovery/file"
"github.com/prometheus/prometheus/discovery/hetzner"
"github.com/prometheus/prometheus/discovery/kubernetes"
"github.com/prometheus/prometheus/discovery/marathon"
"github.com/prometheus/prometheus/discovery/openstack"
@ -648,6 +649,34 @@ var expectedConf = &Config{
}},
},
},
{
JobName: "hetzner",
HonorTimestamps: true,
ScrapeInterval: model.Duration(15 * time.Second),
ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
ServiceDiscoveryConfigs: discovery.Configs{
&hetzner.SDConfig{
HTTPClientConfig: config.HTTPClientConfig{
BearerToken: "abcdef",
},
Port: 80,
RefreshInterval: model.Duration(60 * time.Second),
Role: "hcloud",
},
&hetzner.SDConfig{
HTTPClientConfig: config.HTTPClientConfig{
BasicAuth: &config.BasicAuth{Username: "abcdef", Password: "abcdef"},
},
Port: 80,
RefreshInterval: model.Duration(60 * time.Second),
Role: "robot",
},
},
},
},
AlertingConfig: AlertingConfig{
AlertmanagerConfigs: []*AlertmanagerConfig{
@ -717,7 +746,7 @@ func TestElideSecrets(t *testing.T) {
yamlConfig := string(config)
matches := secretRe.FindAllStringIndex(yamlConfig, -1)
testutil.Assert(t, len(matches) == 8, "wrong number of secret matches found")
testutil.Assert(t, len(matches) == 10, "wrong number of secret matches found")
testutil.Assert(t, !strings.Contains(yamlConfig, "mysecret"),
"yaml marshal reveals authentication credentials.")
}
@ -963,6 +992,10 @@ var expectedErrors = []struct {
filename: "empty_static_config.bad.yml",
errMsg: "empty or null section in static_configs",
},
{
filename: "hetzner_role.bad.yml",
errMsg: "unknown role",
},
}
func TestBadConfigs(t *testing.T) {

View file

@ -279,6 +279,15 @@ scrape_configs:
cert_file: valid_cert_file
key_file: valid_key_file
- job_name: hetzner
hetzner_sd_configs:
- role: hcloud
bearer_token: abcdef
- role: robot
basic_auth:
username: abcdef
password: abcdef
alerting:
alertmanagers:
- scheme: https

4
config/testdata/hetzner_role.bad.yml vendored Normal file
View file

@ -0,0 +1,4 @@
scrape_configs:
- hetzner_sd_configs:
- role: invalid

132
discovery/hetzner/hcloud.go Normal file
View file

@ -0,0 +1,132 @@
// 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 hetzner
import (
"context"
"fmt"
"net"
"net/http"
"strconv"
"time"
"github.com/go-kit/kit/log"
"github.com/hetznercloud/hcloud-go/hcloud"
"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"
"github.com/prometheus/prometheus/util/strutil"
)
const (
hetznerHcloudLabelPrefix = hetznerLabelPrefix + "hcloud_"
hetznerLabelHcloudImageName = hetznerHcloudLabelPrefix + "image_name"
hetznerLabelHcloudImageDescription = hetznerHcloudLabelPrefix + "image_description"
hetznerLabelHcloudImageOSVersion = hetznerHcloudLabelPrefix + "image_os_version"
hetznerLabelHcloudImageOSFlavor = hetznerHcloudLabelPrefix + "image_os_flavor"
hetznerLabelHcloudPrivateIPv4 = hetznerHcloudLabelPrefix + "private_ipv4_"
hetznerLabelHcloudDatacenterLocation = hetznerHcloudLabelPrefix + "datacenter_location"
hetznerLabelHcloudDatacenterLocationNetworkZone = hetznerHcloudLabelPrefix + "datacenter_location_network_zone"
hetznerLabelHcloudCPUCores = hetznerHcloudLabelPrefix + "cpu_cores"
hetznerLabelHcloudCPUType = hetznerHcloudLabelPrefix + "cpu_type"
hetznerLabelHcloudMemoryGB = hetznerHcloudLabelPrefix + "memory_size_gb"
hetznerLabelHcloudDiskGB = hetznerHcloudLabelPrefix + "disk_size_gb"
hetznerLabelHcloudType = hetznerHcloudLabelPrefix + "server_type"
hetznerLabelHcloudLabel = hetznerHcloudLabelPrefix + "label_"
)
// Discovery periodically performs Hetzner Cloud requests. It implements
// the Discoverer interface.
type hcloudDiscovery struct {
*refresh.Discovery
client *hcloud.Client
port int
}
// newHcloudDiscovery returns a new hcloudDiscovery which periodically refreshes its targets.
func newHcloudDiscovery(conf *SDConfig, logger log.Logger) (*hcloudDiscovery, error) {
d := &hcloudDiscovery{
port: conf.Port,
}
rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "hetzner_sd", false, false)
if err != nil {
return nil, err
}
d.client = hcloud.NewClient(
hcloud.WithApplication("Prometheus", version.Version),
hcloud.WithHTTPClient(&http.Client{
Transport: rt,
Timeout: time.Duration(conf.RefreshInterval),
}),
hcloud.WithEndpoint(conf.hcloudEndpoint),
)
return d, nil
}
func (d *hcloudDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
servers, err := d.client.Server.All(ctx)
if err != nil {
return nil, err
}
networks, err := d.client.Network.All(ctx)
if err != nil {
return nil, err
}
targets := make([]model.LabelSet, len(servers))
for i, server := range servers {
labels := model.LabelSet{
hetznerLabelRole: model.LabelValue(hetznerRoleHcloud),
hetznerLabelServerID: model.LabelValue(fmt.Sprintf("%d", server.ID)),
hetznerLabelServerName: model.LabelValue(server.Name),
hetznerLabelDatacenter: model.LabelValue(server.Datacenter.Name),
hetznerLabelPublicIPv4: model.LabelValue(server.PublicNet.IPv4.IP.String()),
hetznerLabelPublicIPv6Network: model.LabelValue(server.PublicNet.IPv6.Network.String()),
hetznerLabelServerStatus: model.LabelValue(server.Status),
hetznerLabelHcloudDatacenterLocation: model.LabelValue(server.Datacenter.Location.Name),
hetznerLabelHcloudDatacenterLocationNetworkZone: model.LabelValue(server.Datacenter.Location.NetworkZone),
hetznerLabelHcloudType: model.LabelValue(server.ServerType.Name),
hetznerLabelHcloudCPUCores: model.LabelValue(fmt.Sprintf("%d", server.ServerType.Cores)),
hetznerLabelHcloudCPUType: model.LabelValue(server.ServerType.CPUType),
hetznerLabelHcloudMemoryGB: model.LabelValue(fmt.Sprintf("%d", int(server.ServerType.Memory))),
hetznerLabelHcloudDiskGB: model.LabelValue(fmt.Sprintf("%d", server.ServerType.Disk)),
model.AddressLabel: model.LabelValue(net.JoinHostPort(server.PublicNet.IPv4.IP.String(), strconv.FormatUint(uint64(d.port), 10))),
}
if server.Image != nil {
labels[hetznerLabelHcloudImageName] = model.LabelValue(server.Image.Name)
labels[hetznerLabelHcloudImageDescription] = model.LabelValue(server.Image.Description)
labels[hetznerLabelHcloudImageOSVersion] = model.LabelValue(server.Image.OSVersion)
labels[hetznerLabelHcloudImageOSFlavor] = model.LabelValue(server.Image.OSFlavor)
}
for _, privateNet := range server.PrivateNet {
for _, network := range networks {
if privateNet.Network.ID == network.ID {
networkLabel := model.LabelName(hetznerLabelHcloudPrivateIPv4 + strutil.SanitizeLabelName(network.Name))
labels[networkLabel] = model.LabelValue(privateNet.IP.String())
}
}
}
for labelKey, labelValue := range server.Labels {
label := model.LabelName(hetznerLabelHcloudLabel + strutil.SanitizeLabelName(labelKey))
labels[label] = model.LabelValue(labelValue)
}
targets[i] = labels
}
return []*targetgroup.Group{{Source: "hetzner", Targets: targets}}, nil
}

View file

@ -0,0 +1,124 @@
// 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 hetzner
import (
"context"
"fmt"
"github.com/go-kit/kit/log"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/util/testutil"
"testing"
)
type hcloudSDTestSuite struct {
Mock *SDMock
}
func (s *hcloudSDTestSuite) SetupTest(t *testing.T) {
s.Mock = NewSDMock(t)
s.Mock.Setup()
s.Mock.HandleHcloudServers()
s.Mock.HandleHcloudNetworks()
}
func TestHCloudSDRefresh(t *testing.T) {
suite := &hcloudSDTestSuite{}
suite.SetupTest(t)
cfg := DefaultSDConfig
cfg.HTTPClientConfig.BearerToken = hcloudTestToken
cfg.hcloudEndpoint = suite.Mock.Endpoint()
d, err := newHcloudDiscovery(&cfg, log.NewNopLogger())
testutil.Ok(t, err)
targetGroups, err := d.refresh(context.Background())
testutil.Ok(t, err)
testutil.Equals(t, 1, len(targetGroups))
targetGroup := targetGroups[0]
testutil.Assert(t, targetGroup != nil, "targetGroup should not be nil")
testutil.Assert(t, targetGroup.Targets != nil, "targetGroup.targets should not be nil")
testutil.Equals(t, 3, len(targetGroup.Targets))
for i, labelSet := range []model.LabelSet{
{
"__address__": model.LabelValue("1.2.3.4:80"),
"__meta_hetzner_role": model.LabelValue("hcloud"),
"__meta_hetzner_server_id": model.LabelValue("42"),
"__meta_hetzner_server_name": model.LabelValue("my-server"),
"__meta_hetzner_server_status": model.LabelValue("running"),
"__meta_hetzner_public_ipv4": model.LabelValue("1.2.3.4"),
"__meta_hetzner_public_ipv6_network": model.LabelValue("2001:db8::/64"),
"__meta_hetzner_datacenter": model.LabelValue("fsn1-dc8"),
"__meta_hetzner_hcloud_image_name": model.LabelValue("ubuntu-20.04"),
"__meta_hetzner_hcloud_image_description": model.LabelValue("Ubuntu 20.04 Standard 64 bit"),
"__meta_hetzner_hcloud_image_os_flavor": model.LabelValue("ubuntu"),
"__meta_hetzner_hcloud_image_os_version": model.LabelValue("20.04"),
"__meta_hetzner_hcloud_datacenter_location": model.LabelValue("fsn1"),
"__meta_hetzner_hcloud_datacenter_location_network_zone": model.LabelValue("eu-central"),
"__meta_hetzner_hcloud_cpu_cores": model.LabelValue("1"),
"__meta_hetzner_hcloud_cpu_type": model.LabelValue("shared"),
"__meta_hetzner_hcloud_memory_size_gb": model.LabelValue("1"),
"__meta_hetzner_hcloud_disk_size_gb": model.LabelValue("25"),
"__meta_hetzner_hcloud_server_type": model.LabelValue("cx11"),
"__meta_hetzner_hcloud_private_ipv4_mynet": model.LabelValue("10.0.0.2"),
"__meta_hetzner_hcloud_label_my_key": model.LabelValue("my-value"),
},
{
"__address__": model.LabelValue("1.2.3.5:80"),
"__meta_hetzner_role": model.LabelValue("hcloud"),
"__meta_hetzner_server_id": model.LabelValue("44"),
"__meta_hetzner_server_name": model.LabelValue("another-server"),
"__meta_hetzner_server_status": model.LabelValue("stopped"),
"__meta_hetzner_datacenter": model.LabelValue("fsn1-dc14"),
"__meta_hetzner_public_ipv4": model.LabelValue("1.2.3.5"),
"__meta_hetzner_public_ipv6_network": model.LabelValue("2001:db9::/64"),
"__meta_hetzner_hcloud_image_name": model.LabelValue("ubuntu-20.04"),
"__meta_hetzner_hcloud_image_description": model.LabelValue("Ubuntu 20.04 Standard 64 bit"),
"__meta_hetzner_hcloud_image_os_flavor": model.LabelValue("ubuntu"),
"__meta_hetzner_hcloud_image_os_version": model.LabelValue("20.04"),
"__meta_hetzner_hcloud_datacenter_location": model.LabelValue("fsn1"),
"__meta_hetzner_hcloud_datacenter_location_network_zone": model.LabelValue("eu-central"),
"__meta_hetzner_hcloud_cpu_cores": model.LabelValue("2"),
"__meta_hetzner_hcloud_cpu_type": model.LabelValue("shared"),
"__meta_hetzner_hcloud_memory_size_gb": model.LabelValue("1"),
"__meta_hetzner_hcloud_disk_size_gb": model.LabelValue("50"),
"__meta_hetzner_hcloud_server_type": model.LabelValue("cpx11"),
},
{
"__address__": model.LabelValue("1.2.3.6:80"),
"__meta_hetzner_role": model.LabelValue("hcloud"),
"__meta_hetzner_server_id": model.LabelValue("36"),
"__meta_hetzner_server_name": model.LabelValue("deleted-image-server"),
"__meta_hetzner_server_status": model.LabelValue("stopped"),
"__meta_hetzner_datacenter": model.LabelValue("fsn1-dc14"),
"__meta_hetzner_public_ipv4": model.LabelValue("1.2.3.6"),
"__meta_hetzner_public_ipv6_network": model.LabelValue("2001:db7::/64"),
"__meta_hetzner_hcloud_datacenter_location": model.LabelValue("fsn1"),
"__meta_hetzner_hcloud_datacenter_location_network_zone": model.LabelValue("eu-central"),
"__meta_hetzner_hcloud_cpu_cores": model.LabelValue("2"),
"__meta_hetzner_hcloud_cpu_type": model.LabelValue("shared"),
"__meta_hetzner_hcloud_memory_size_gb": model.LabelValue("1"),
"__meta_hetzner_hcloud_disk_size_gb": model.LabelValue("50"),
"__meta_hetzner_hcloud_server_type": model.LabelValue("cpx11"),
},
} {
t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
testutil.Equals(t, labelSet, targetGroup.Targets[i])
})
}
}

View file

@ -0,0 +1,150 @@
// 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 hetzner
import (
"context"
"github.com/pkg/errors"
"time"
"github.com/go-kit/kit/log"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/refresh"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
const (
hetznerLabelPrefix = model.MetaLabelPrefix + "hetzner_"
hetznerLabelRole = hetznerLabelPrefix + "role"
hetznerLabelServerID = hetznerLabelPrefix + "server_id"
hetznerLabelServerName = hetznerLabelPrefix + "server_name"
hetznerLabelServerStatus = hetznerLabelPrefix + "server_status"
hetznerLabelDatacenter = hetznerLabelPrefix + "datacenter"
hetznerLabelPublicIPv4 = hetznerLabelPrefix + "public_ipv4"
hetznerLabelPublicIPv6Network = hetznerLabelPrefix + "public_ipv6_network"
)
// DefaultSDConfig is the default Hetzner SD configuration.
var DefaultSDConfig = SDConfig{
Port: 80,
RefreshInterval: model.Duration(60 * time.Second),
}
func init() {
discovery.RegisterConfig(&SDConfig{})
}
// SDConfig is the configuration for Hetzner based service discovery.
type SDConfig struct {
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
RefreshInterval model.Duration `yaml:"refresh_interval"`
Port int `yaml:"port"`
Role role `yaml:"role"`
hcloudEndpoint string // For tests only.
robotEndpoint string // For tests only.
}
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "hetzner" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
return NewDiscovery(c, opts.Logger)
}
type refresher interface {
refresh(context.Context) ([]*targetgroup.Group, error)
}
// role is the role of the target within the Hetzner Ecosystem.
type role string
// The valid options for role.
const (
// Hetzner Robot Role (Dedicated Server)
// https://robot.hetzner.com
hetznerRoleRobot role = "robot"
// Hetzner Cloud Role
// https://console.hetzner.cloud
hetznerRoleHcloud role = "hcloud"
)
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *role) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal((*string)(c)); err != nil {
return err
}
switch *c {
case hetznerRoleRobot, hetznerRoleHcloud:
return nil
default:
return errors.Errorf("unknown role %q", *c)
}
}
// 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
}
if c.Role == "" {
return errors.New("role missing (one of: robot, hcloud)")
}
return nil
}
// Discovery periodically performs Hetzner requests. It implements
// the Discoverer interface.
type Discovery struct {
*refresh.Discovery
}
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error) {
r, err := newRefresher(conf, logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
logger,
"hetzner",
time.Duration(conf.RefreshInterval),
r.refresh,
), nil
}
func newRefresher(conf *SDConfig, l log.Logger) (refresher, error) {
switch conf.Role {
case hetznerRoleHcloud:
if conf.hcloudEndpoint == "" {
conf.hcloudEndpoint = hcloud.Endpoint
}
return newHcloudDiscovery(conf, l)
case hetznerRoleRobot:
if conf.robotEndpoint == "" {
conf.robotEndpoint = "https://robot-ws.your-server.de"
}
return newRobotDiscovery(conf, l)
}
return nil, errors.New("unknown Hetzner discovery role")
}

View file

@ -0,0 +1,552 @@
// 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 hetzner
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
// SDMock is the interface for the Hetzner Cloud 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)
m.t.Cleanup(m.Server.Close)
}
// ShutdownServer creates the mock server
func (m *SDMock) ShutdownServer() {
m.Server.Close()
}
const hcloudTestToken = "LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"
// HandleHcloudServers mocks the cloud servers list endpoint.
func (m *SDMock) HandleHcloudServers() {
m.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", hcloudTestToken) {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Add("content-type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `
{
"servers": [
{
"id": 42,
"name": "my-server",
"status": "running",
"created": "2016-01-30T23:50:00+00:00",
"public_net": {
"ipv4": {
"ip": "1.2.3.4",
"blocked": false,
"dns_ptr": "server01.example.com"
},
"ipv6": {
"ip": "2001:db8::/64",
"blocked": false,
"dns_ptr": [
{
"ip": "2001:db8::1",
"dns_ptr": "server.example.com"
}
]
},
"floating_ips": [
478
]
},
"private_net": [
{
"network": 4711,
"ip": "10.0.0.2",
"alias_ips": [],
"mac_address": "86:00:ff:2a:7d:e1"
}
],
"server_type": {
"id": 1,
"name": "cx11",
"description": "CX11",
"cores": 1,
"memory": 1,
"disk": 25,
"deprecated": false,
"prices": [
{
"location": "fsn1",
"price_hourly": {
"net": "1.0000000000",
"gross": "1.1900000000000000"
},
"price_monthly": {
"net": "1.0000000000",
"gross": "1.1900000000000000"
}
}
],
"storage_type": "local",
"cpu_type": "shared"
},
"datacenter": {
"id": 1,
"name": "fsn1-dc8",
"description": "Falkenstein 1 DC 8",
"location": {
"id": 1,
"name": "fsn1",
"description": "Falkenstein DC Park 1",
"country": "DE",
"city": "Falkenstein",
"latitude": 50.47612,
"longitude": 12.370071,
"network_zone": "eu-central"
},
"server_types": {
"supported": [
1,
2,
3
],
"available": [
1,
2,
3
],
"available_for_migration": [
1,
2,
3
]
}
},
"image": {
"id": 4711,
"type": "system",
"status": "available",
"name": "ubuntu-20.04",
"description": "Ubuntu 20.04 Standard 64 bit",
"image_size": 2.3,
"disk_size": 10,
"created": "2016-01-30T23:50:00+00:00",
"created_from": {
"id": 1,
"name": "Server"
},
"bound_to": null,
"os_flavor": "ubuntu",
"os_version": "20.04",
"rapid_deploy": false,
"protection": {
"delete": false
},
"deprecated": "2018-02-28T00:00:00+00:00",
"labels": {}
},
"iso": null,
"rescue_enabled": false,
"locked": false,
"backup_window": "22-02",
"outgoing_traffic": 123456,
"ingoing_traffic": 123456,
"included_traffic": 654321,
"protection": {
"delete": false,
"rebuild": false
},
"labels": {
"my-key": "my-value"
},
"volumes": [],
"load_balancers": []
},
{
"id": 44,
"name": "another-server",
"status": "stopped",
"created": "2016-01-30T23:50:00+00:00",
"public_net": {
"ipv4": {
"ip": "1.2.3.5",
"blocked": false,
"dns_ptr": "server01.example.org"
},
"ipv6": {
"ip": "2001:db9::/64",
"blocked": false,
"dns_ptr": [
{
"ip": "2001:db9::1",
"dns_ptr": "server01.example.org"
}
]
},
"floating_ips": []
},
"private_net": [],
"server_type": {
"id": 2,
"name": "cpx11",
"description": "CPX11",
"cores": 2,
"memory": 1,
"disk": 50,
"deprecated": false,
"prices": [
{
"location": "fsn1",
"price_hourly": {
"net": "1.0000000000",
"gross": "1.1900000000000000"
},
"price_monthly": {
"net": "1.0000000000",
"gross": "1.1900000000000000"
}
}
],
"storage_type": "local",
"cpu_type": "shared"
},
"datacenter": {
"id": 2,
"name": "fsn1-dc14",
"description": "Falkenstein 1 DC 14",
"location": {
"id": 1,
"name": "fsn1",
"description": "Falkenstein DC Park 1",
"country": "DE",
"city": "Falkenstein",
"latitude": 50.47612,
"longitude": 12.370071,
"network_zone": "eu-central"
},
"server_types": {
"supported": [
1,
2,
3
],
"available": [
1,
2,
3
],
"available_for_migration": [
1,
2,
3
]
}
},
"image": {
"id": 4711,
"type": "system",
"status": "available",
"name": "ubuntu-20.04",
"description": "Ubuntu 20.04 Standard 64 bit",
"image_size": 2.3,
"disk_size": 10,
"created": "2016-01-30T23:50:00+00:00",
"created_from": {
"id": 1,
"name": "Server"
},
"bound_to": null,
"os_flavor": "ubuntu",
"os_version": "20.04",
"rapid_deploy": false,
"protection": {
"delete": false
},
"deprecated": "2018-02-28T00:00:00+00:00",
"labels": {}
},
"iso": null,
"rescue_enabled": false,
"locked": false,
"backup_window": "22-02",
"outgoing_traffic": 123456,
"ingoing_traffic": 123456,
"included_traffic": 654321,
"protection": {
"delete": false,
"rebuild": false
},
"labels": {},
"volumes": [],
"load_balancers": []
},
{
"id": 36,
"name": "deleted-image-server",
"status": "stopped",
"created": "2016-01-30T23:50:00+00:00",
"public_net": {
"ipv4": {
"ip": "1.2.3.6",
"blocked": false,
"dns_ptr": "server01.example.org"
},
"ipv6": {
"ip": "2001:db7::/64",
"blocked": false,
"dns_ptr": [
{
"ip": "2001:db7::1",
"dns_ptr": "server01.example.org"
}
]
},
"floating_ips": []
},
"private_net": [],
"server_type": {
"id": 2,
"name": "cpx11",
"description": "CPX11",
"cores": 2,
"memory": 1,
"disk": 50,
"deprecated": false,
"prices": [
{
"location": "fsn1",
"price_hourly": {
"net": "1.0000000000",
"gross": "1.1900000000000000"
},
"price_monthly": {
"net": "1.0000000000",
"gross": "1.1900000000000000"
}
}
],
"storage_type": "local",
"cpu_type": "shared"
},
"datacenter": {
"id": 2,
"name": "fsn1-dc14",
"description": "Falkenstein 1 DC 14",
"location": {
"id": 1,
"name": "fsn1",
"description": "Falkenstein DC Park 1",
"country": "DE",
"city": "Falkenstein",
"latitude": 50.47612,
"longitude": 12.370071,
"network_zone": "eu-central"
},
"server_types": {
"supported": [
1,
2,
3
],
"available": [
1,
2,
3
],
"available_for_migration": [
1,
2,
3
]
}
},
"image": null,
"iso": null,
"rescue_enabled": false,
"locked": false,
"backup_window": "22-02",
"outgoing_traffic": 123456,
"ingoing_traffic": 123456,
"included_traffic": 654321,
"protection": {
"delete": false,
"rebuild": false
},
"labels": {},
"volumes": [],
"load_balancers": []
}
],
"meta": {
"pagination": {
"page": 1,
"per_page": 25,
"previous_page": null,
"next_page": null,
"last_page": 1,
"total_entries": 2
}
}
}`,
)
})
}
// HandleHcloudNetworks mocks the cloud networks list endpoint.
func (m *SDMock) HandleHcloudNetworks() {
m.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", hcloudTestToken) {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Add("content-type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `
{
"networks": [
{
"id": 4711,
"name": "mynet",
"ip_range": "10.0.0.0/16",
"subnets": [
{
"type": "cloud",
"ip_range": "10.0.1.0/24",
"network_zone": "eu-central",
"gateway": "10.0.0.1"
}
],
"routes": [
{
"destination": "10.100.1.0/24",
"gateway": "10.0.1.1"
}
],
"servers": [
42
],
"load_balancers": [
42
],
"protection": {
"delete": false
},
"labels": {},
"created": "2016-01-30T23:50:00+00:00"
}
],
"meta": {
"pagination": {
"page": 1,
"per_page": 25,
"previous_page": null,
"next_page": null,
"last_page": 1,
"total_entries": 1
}
}
}`,
)
})
}
const robotTestUsername = "my-hetzner"
const robotTestPassword = "my-password"
// HandleRobotServers mocks the robot servers list endpoint.
func (m *SDMock) HandleRobotServers() {
m.Mux.HandleFunc("/server", func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if username != robotTestUsername && password != robotTestPassword && !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Add("content-type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `
[
{
"server":{
"server_ip":"123.123.123.123",
"server_number":321,
"server_name":"server1",
"product":"DS 3000",
"dc":"NBG1-DC1",
"traffic":"5 TB",
"flatrate":true,
"status":"ready",
"throttled":true,
"cancelled":false,
"paid_until":"2010-09-02",
"ip":[
"123.123.123.123"
],
"subnet":[
{
"ip":"2a01:4f8:111:4221::",
"mask":"64"
}
]
}
},
{
"server":{
"server_ip":"123.123.123.124",
"server_number":421,
"server_name":"server2",
"product":"X5",
"dc":"FSN1-DC10",
"traffic":"2 TB",
"flatrate":true,
"status":"in process",
"throttled":false,
"cancelled":true,
"paid_until":"2010-06-11",
"ip":[
"123.123.123.124"
],
"subnet":null
}
}
]`,
)
})
}

124
discovery/hetzner/robot.go Normal file
View file

@ -0,0 +1,124 @@
// 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 hetzner
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-kit/kit/log"
config_util "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/discovery/refresh"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
const (
hetznerRobotLabelPrefix = hetznerLabelPrefix + "robot_"
hetznerLabelRobotProduct = hetznerRobotLabelPrefix + "product"
hetznerLabelRobotCancelled = hetznerRobotLabelPrefix + "cancelled"
)
// Discovery periodically performs Hetzner Robot requests. It implements
// the Discoverer interface.
type robotDiscovery struct {
*refresh.Discovery
client *http.Client
port int
endpoint string
}
// newRobotDiscovery returns a new robotDiscovery which periodically refreshes its targets.
func newRobotDiscovery(conf *SDConfig, logger log.Logger) (*robotDiscovery, error) {
d := &robotDiscovery{
port: conf.Port,
endpoint: conf.robotEndpoint,
}
rt, err := config_util.NewRoundTripperFromConfig(conf.HTTPClientConfig, "hetzner_sd", false, false)
if err != nil {
return nil, err
}
d.client = &http.Client{
Transport: rt,
Timeout: time.Duration(conf.RefreshInterval),
}
return d, nil
}
func (d *robotDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
resp, err := d.client.Get(d.endpoint + "/server")
if err != nil {
return nil, err
}
defer func() {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}()
var servers serversList
err = json.NewDecoder(resp.Body).Decode(&servers)
if err != nil {
return nil, err
}
targets := make([]model.LabelSet, len(servers))
for i, server := range servers {
labels := model.LabelSet{
hetznerLabelRole: model.LabelValue(hetznerRoleRobot),
hetznerLabelServerID: model.LabelValue(strconv.Itoa(server.Server.ServerNumber)),
hetznerLabelServerName: model.LabelValue(server.Server.ServerName),
hetznerLabelDatacenter: model.LabelValue(strings.ToLower(server.Server.Dc)),
hetznerLabelPublicIPv4: model.LabelValue(server.Server.ServerIP),
hetznerLabelServerStatus: model.LabelValue(server.Server.Status),
hetznerLabelRobotProduct: model.LabelValue(server.Server.Product),
hetznerLabelRobotCancelled: model.LabelValue(fmt.Sprintf("%t", server.Server.Canceled)),
model.AddressLabel: model.LabelValue(net.JoinHostPort(server.Server.ServerIP, strconv.FormatUint(uint64(d.port), 10))),
}
for _, subnet := range server.Server.Subnet {
ip := net.ParseIP(subnet.IP)
if ip.To4() == nil {
labels[hetznerLabelPublicIPv6Network] = model.LabelValue(fmt.Sprintf("%s/%s", subnet.IP, subnet.Mask))
break
}
}
targets[i] = labels
}
return []*targetgroup.Group{{Source: "hetzner", Targets: targets}}, nil
}
type serversList []struct {
Server struct {
ServerIP string `json:"server_ip"`
ServerNumber int `json:"server_number"`
ServerName string `json:"server_name"`
Dc string `json:"dc"`
Status string `json:"status"`
Product string `json:"product"`
Canceled bool `json:"cancelled"`
Subnet []struct {
IP string `json:"ip"`
Mask string `json:"mask"`
} `json:"subnet"`
} `json:"server"`
}

View file

@ -0,0 +1,86 @@
// 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 hetzner
import (
"context"
"fmt"
"github.com/go-kit/kit/log"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/util/testutil"
"testing"
)
type robotSDTestSuite struct {
Mock *SDMock
}
func (s *robotSDTestSuite) SetupTest(t *testing.T) {
s.Mock = NewSDMock(t)
s.Mock.Setup()
s.Mock.HandleRobotServers()
}
func TestRobotSDRefresh(t *testing.T) {
suite := &robotSDTestSuite{}
suite.SetupTest(t)
cfg := DefaultSDConfig
cfg.HTTPClientConfig.BasicAuth = &config.BasicAuth{Username: robotTestUsername, Password: robotTestPassword}
cfg.robotEndpoint = suite.Mock.Endpoint()
d, err := newRobotDiscovery(&cfg, log.NewNopLogger())
testutil.Ok(t, err)
targetGroups, err := d.refresh(context.Background())
testutil.Ok(t, err)
testutil.Equals(t, 1, len(targetGroups))
targetGroup := targetGroups[0]
testutil.Assert(t, targetGroup != nil, "targetGroup should not be nil")
testutil.Assert(t, targetGroup.Targets != nil, "targetGroup.targets should not be nil")
testutil.Equals(t, 2, len(targetGroup.Targets))
for i, labelSet := range []model.LabelSet{
{
"__address__": model.LabelValue("123.123.123.123:80"),
"__meta_hetzner_role": model.LabelValue("robot"),
"__meta_hetzner_server_id": model.LabelValue("321"),
"__meta_hetzner_server_name": model.LabelValue("server1"),
"__meta_hetzner_server_status": model.LabelValue("ready"),
"__meta_hetzner_public_ipv4": model.LabelValue("123.123.123.123"),
"__meta_hetzner_public_ipv6_network": model.LabelValue("2a01:4f8:111:4221::/64"),
"__meta_hetzner_datacenter": model.LabelValue("nbg1-dc1"),
"__meta_hetzner_robot_product": model.LabelValue("DS 3000"),
"__meta_hetzner_robot_cancelled": model.LabelValue("false"),
},
{
"__address__": model.LabelValue("123.123.123.124:80"),
"__meta_hetzner_role": model.LabelValue("robot"),
"__meta_hetzner_server_id": model.LabelValue("421"),
"__meta_hetzner_server_name": model.LabelValue("server2"),
"__meta_hetzner_server_status": model.LabelValue("in process"),
"__meta_hetzner_public_ipv4": model.LabelValue("123.123.123.124"),
"__meta_hetzner_datacenter": model.LabelValue("fsn1-dc10"),
"__meta_hetzner_robot_product": model.LabelValue("X5"),
"__meta_hetzner_robot_cancelled": model.LabelValue("true"),
},
} {
t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
testutil.Equals(t, labelSet, targetGroup.Targets[i])
})
}
}

View file

@ -24,6 +24,7 @@ import (
_ "github.com/prometheus/prometheus/discovery/ec2" // register ec2
_ "github.com/prometheus/prometheus/discovery/file" // register file
_ "github.com/prometheus/prometheus/discovery/gce" // register gce
_ "github.com/prometheus/prometheus/discovery/hetzner" // register hetzner
_ "github.com/prometheus/prometheus/discovery/kubernetes" // register kubernetes
_ "github.com/prometheus/prometheus/discovery/marathon" // register marathon
_ "github.com/prometheus/prometheus/discovery/openstack" // register openstack

View file

@ -215,6 +215,10 @@ file_sd_configs:
gce_sd_configs:
[ - <gce_sd_config> ... ]
# List of Hetzner service discovery configurations.
hetzner_sd_configs:
[ - <hetzner_sd_config> ... ]
# List of Kubernetes service discovery configurations.
kubernetes_sd_configs:
[ - <kubernetes_sd_config> ... ]
@ -918,6 +922,81 @@ instance it is running on should have at least read-only permissions to the
compute resources. If running outside of GCE make sure to create an appropriate
service account and place the credential file in one of the expected locations.
### `<hetzner_sd_config>`
Hetzner SD configurations allow retrieving scrape targets from [Hetzner Cloud](https://www.hetzner.cloud/)
Server API.
This service discovery uses the public IPv4 address by default, but that can be
changed with relabeling, as demonstrated in [the Prometheus hetzner-sd
configuration file](/documentation/examples/prometheus-hetzner.yml).
The following meta labels are available on all targets during [relabeling](#relabel_config):
* `__meta_hetzner_server_id`: the id of the server
* `__meta_hetzner_server_name`: the name of the server
* `__meta_hetzner_server_status`: the status of the server
* `__meta_hetzner_public_ipv4`: the public ipv4 of the server
* `__meta_hetzner_public_ipv6_network`: the public ipv6 net (/64) of the server
* `__meta_hetzner_datacenter`: the name of the datacenter the server is located
The labels below are only available for targets with `role` set to `hcloud`:
* `__meta_hetzner_hcloud_image_name`: the image name of the server
* `__meta_hetzner_hcloud_image_description`: the image description of the server
* `__meta_hetzner_hcloud_image_os_flavor`: the image os flavor of the server
* `__meta_hetzner_hcloud_image_os_version`: the image os version of the server
* `__meta_hetzner_hcloud_image_description`: the image name or description of the server
* `__meta_hetzner_hcloud_datacenter_location`: the name of the location the server is located
* `__meta_hetzner_hcloud_datacenter_location_network_zone`: the name of the network zone the server is located
* `__meta_hetzner_hcloud_server_type`: the name of the type of the server
* `__meta_hetzner_hcloud_cpu_cores`: the count of CPU cores the server has
* `__meta_hetzner_hcloud_cpu_type`: the type of the CPU (shared or dedicated)
* `__meta_hetzner_hcloud_memory_size_gb`: the amount of memory the server has (in GB)
* `__meta_hetzner_hcloud_disk_size_gb`: the size of disk the server has (in GB)
* `__meta_hetzner_hcloud_private_ipv4_<network name>`: the private ipv4 of the server within the network named in the metric if it is attached to a network
* `__meta_hetzner_hcloud_label_<labelname>`: the hetzner cloud label of the server with its specific value within the cloud
The labels below are only available for targets with `role` set to `robot`:
* `__meta_hetzner_robot_product`: the name of the product of the server
* `__meta_hetzner_robot_cancelled`: the status of the server cancellation
```yaml
# The Hetzner role of entities that should be discovered.
# One of robot or hcloud.
role: <string>
# Authentication information used to authenticate to the API server.
# Note that `basic_auth`, `bearer_token` and `bearer_token_file` options are
# mutually exclusive.
# password and password_file are mutually exclusive.
# Optional HTTP basic authentication information, required when role is robot
# Role hcloud does not support basic auth.
basic_auth:
[ username: <string> ]
[ password: <secret> ]
[ password_file: <string> ]
# Optional bearer token authentication information, required when role is hcloud
# Role robot does not support bearer token authentication.
[ bearer_token: <secret> ]
# Optional bearer token file authentication information.
[ bearer_token_file: <filename> ]
# Optional proxy URL.
[ proxy_url: <string> ]
# TLS configuration.
tls_config:
[ <tls_config> ]
# The port to scrape metrics from.
[ port: <int> | default = 80 ]
# The time after which the servers are refreshed.
[ refresh_interval: <duration> | default = 60s ]
```
### `<kubernetes_sd_config>`
Kubernetes SD configurations allow retrieving scrape targets from
@ -1494,6 +1573,10 @@ dockerswarm_sd_configs:
gce_sd_configs:
[ - <gce_sd_config> ... ]
# List of Hetzner service discovery configurations.
hetzner_sd_configs:
[ - <hetzner_sd_config> ... ]
# List of Kubernetes service discovery configurations.
kubernetes_sd_configs:
[ - <kubernetes_sd_config> ... ]

View file

@ -0,0 +1,47 @@
# A example scrape configuration for running Prometheus with
# Hetzner.
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'
hetzner_sd_configs:
- bearer_token: "<replace with a Hetzner Cloud API Token>"
platform: "hcloud"
relabel_configs:
# Use the public IPv4 and port 9100 to scrape the target.
- source_labels: [__meta_hetzner_public_ipv4]
target_label: __address__
replacement: '$1:9100'
# Discover Node Exporter instances to scrape using a Hetzner Cloud Network called mynet.
- job_name: 'node_private'
hetzner_sd_configs:
- bearer_token: "<replace with a Hetzner Cloud API Token>"
platform: "hcloud"
relabel_configs:
# Use the private IPv4 within the Hetzner Cloud Network and port 9100 to scrape the target.
- source_labels: [__meta_hetzner_hcloud_private_ipv4_mynet]
target_label: __address__
replacement: '$1:9100'
# Discover Node Exporter instances to scrape.
- job_name: 'node_robot'
hetzner_sd_configs:
- basic_auth:
username: "<replace with a Hetzner Robot API username>"
password: "<replace with a Hetzner Robot API password>"
platform: "robot"
relabel_configs:
# Use the public IPv4 and port 9100 to scrape the target.
- source_labels: [__meta_hetzner_public_ipv4]
target_label: __address__
replacement: '$1:9100'

1
go.mod
View file

@ -35,6 +35,7 @@ require (
github.com/hashicorp/go-hclog v0.12.2 // indirect
github.com/hashicorp/go-immutable-radix v1.2.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hetznercloud/hcloud-go v1.21.1
github.com/influxdata/influxdb v1.8.1
github.com/json-iterator/go v1.1.10
github.com/mattn/go-colorable v0.1.6 // indirect

4
go.sum
View file

@ -351,6 +351,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -440,6 +442,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.9.0 h1:+Zd/16AJ9lxk9RzfTDyv/TLhZ8UerqYS0/+JGCIDaa0=
github.com/hashicorp/serf v0.9.0/go.mod h1:YL0HO+FifKOW2u1ke99DGVu1zhcpZzNwrLIqBC7vbYU=
github.com/hetznercloud/hcloud-go v1.21.1 h1:LWNozxiZhKmeMqYbAS7KsAcPcxg47afCnTeLKmN+n7w=
github.com/hetznercloud/hcloud-go v1.21.1/go.mod h1:xng8lbDUg+xM1dgc0yGHX5EeqbwIq7UYlMWMTx3SQVg=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=

View file

@ -6,6 +6,10 @@
//
// This package is intended to be a more powerful and safer alternative to
// reflect.DeepEqual for comparing whether two values are semantically equal.
// It is intended to only be used in tests, as performance is not a goal and
// it may panic if it cannot compare the values. Its propensity towards
// panicking means that its unsuitable for production environments where a
// spurious panic may be fatal.
//
// The primary features of cmp are:
//
@ -86,6 +90,52 @@ import (
// If there is a cycle, then the pointed at values are considered equal
// only if both addresses were previously visited in the same path step.
func Equal(x, y interface{}, opts ...Option) bool {
s := newState(opts)
s.compareAny(rootStep(x, y))
return s.result.Equal()
}
// Diff returns a human-readable report of the differences between two values.
// It returns an empty string if and only if Equal returns true for the same
// input values and options.
//
// The output is displayed as a literal in pseudo-Go syntax.
// At the start of each line, a "-" prefix indicates an element removed from x,
// a "+" prefix to indicates an element added to y, and the lack of a prefix
// indicates an element common to both x and y. If possible, the output
// uses fmt.Stringer.String or error.Error methods to produce more humanly
// readable outputs. In such cases, the string is prefixed with either an
// 's' or 'e' character, respectively, to indicate that the method was called.
//
// Do not depend on this output being stable. If you need the ability to
// programmatically interpret the difference, consider using a custom Reporter.
func Diff(x, y interface{}, opts ...Option) string {
s := newState(opts)
// Optimization: If there are no other reporters, we can optimize for the
// common case where the result is equal (and thus no reported difference).
// This avoids the expensive construction of a difference tree.
if len(s.reporters) == 0 {
s.compareAny(rootStep(x, y))
if s.result.Equal() {
return ""
}
s.result = diff.Result{} // Reset results
}
r := new(defaultReporter)
s.reporters = append(s.reporters, reporter{r})
s.compareAny(rootStep(x, y))
d := r.String()
if (d == "") != s.result.Equal() {
panic("inconsistent difference and equality results")
}
return d
}
// rootStep constructs the first path step. If x and y have differing types,
// then they are stored within an empty interface type.
func rootStep(x, y interface{}) PathStep {
vx := reflect.ValueOf(x)
vy := reflect.ValueOf(y)
@ -108,33 +158,7 @@ func Equal(x, y interface{}, opts ...Option) bool {
t = vx.Type()
}
s := newState(opts)
s.compareAny(&pathStep{t, vx, vy})
return s.result.Equal()
}
// Diff returns a human-readable report of the differences between two values.
// It returns an empty string if and only if Equal returns true for the same
// input values and options.
//
// The output is displayed as a literal in pseudo-Go syntax.
// At the start of each line, a "-" prefix indicates an element removed from x,
// a "+" prefix to indicates an element added to y, and the lack of a prefix
// indicates an element common to both x and y. If possible, the output
// uses fmt.Stringer.String or error.Error methods to produce more humanly
// readable outputs. In such cases, the string is prefixed with either an
// 's' or 'e' character, respectively, to indicate that the method was called.
//
// Do not depend on this output being stable. If you need the ability to
// programmatically interpret the difference, consider using a custom Reporter.
func Diff(x, y interface{}, opts ...Option) string {
r := new(defaultReporter)
eq := Equal(x, y, Options(opts), Reporter(r))
d := r.String()
if (d == "") != eq {
panic("inconsistent difference and equality results")
}
return d
return &pathStep{t, vx, vy}
}
type state struct {
@ -352,7 +376,7 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
// assuming that T is assignable to R.
// Otherwise, it returns the input value as is.
func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
// TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/22143).
// TODO(≥go1.10): Workaround for reflect bug (https://golang.org/issue/22143).
if !flags.AtLeastGo110 {
if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
return reflect.New(t).Elem()
@ -362,6 +386,7 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
}
func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
var addr bool
var vax, vay reflect.Value // Addressable versions of vx and vy
var mayForce, mayForceInit bool
@ -383,6 +408,7 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
// For retrieveUnexportedField to work, the parent struct must
// be addressable. Create a new copy of the values if
// necessary to make them addressable.
addr = vx.CanAddr() || vy.CanAddr()
vax = makeAddressable(vx)
vay = makeAddressable(vy)
}
@ -393,6 +419,7 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
mayForceInit = true
}
step.mayForce = mayForce
step.paddr = addr
step.pvx = vax
step.pvy = vay
step.field = t.Field(i)

View file

@ -10,6 +10,6 @@ import "reflect"
const supportExporters = false
func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value {
func retrieveUnexportedField(reflect.Value, reflect.StructField, bool) reflect.Value {
panic("no support for forcibly accessing unexported fields")
}

View file

@ -17,9 +17,19 @@ const supportExporters = true
// a struct such that the value has read-write permissions.
//
// The parent struct, v, must be addressable, while f must be a StructField
// describing the field to retrieve.
func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value {
// See https://github.com/google/go-cmp/issues/167 for discussion of the
// following expression.
return reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem()
// describing the field to retrieve. If addr is false,
// then the returned value will be shallowed copied to be non-addressable.
func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value {
ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem()
if !addr {
// A field is addressable if and only if the struct is addressable.
// If the original parent value was not addressable, shallow copy the
// value to make it non-addressable to avoid leaking an implementation
// detail of how forcibly exporting a field works.
if ve.Kind() == reflect.Interface && ve.IsNil() {
return reflect.Zero(f.Type)
}
return reflect.ValueOf(ve.Interface()).Convert(f.Type)
}
return ve
}

View file

@ -12,6 +12,13 @@
// is more important than obtaining a minimal Levenshtein distance.
package diff
import (
"math/rand"
"time"
"github.com/google/go-cmp/cmp/internal/flags"
)
// EditType represents a single operation within an edit-script.
type EditType uint8
@ -112,6 +119,8 @@ func (r Result) Similar() bool {
return r.NumSame+1 >= r.NumDiff
}
var randInt = rand.New(rand.NewSource(time.Now().Unix())).Intn(2)
// Difference reports whether two lists of lengths nx and ny are equal
// given the definition of equality provided as f.
//
@ -159,6 +168,17 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
// A vertical edge is equivalent to inserting a symbol from list Y.
// A diagonal edge is equivalent to a matching symbol between both X and Y.
// To ensure flexibility in changing the algorithm in the future,
// introduce some degree of deliberate instability.
// This is achieved by fiddling the zigzag iterator to start searching
// the graph starting from the bottom-right versus than the top-left.
// The result may differ depending on the starting search location,
// but still produces a valid edit script.
zigzagInit := randInt // either 0 or 1
if flags.Deterministic {
zigzagInit = 0
}
// Invariants:
// • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx
// • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny
@ -209,7 +229,7 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
break
}
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
for stop1, stop2, i := false, false, zigzagInit; !(stop1 && stop2) && searchBudget > 0; i++ {
// Search in a diagonal pattern for a match.
z := zigzag(i)
p := point{fwdFrontier.X + z, fwdFrontier.Y - z}

View file

@ -0,0 +1,157 @@
// Copyright 2020, 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.md file.
package value
import (
"reflect"
"strconv"
)
// TypeString is nearly identical to reflect.Type.String,
// but has an additional option to specify that full type names be used.
func TypeString(t reflect.Type, qualified bool) string {
return string(appendTypeName(nil, t, qualified, false))
}
func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte {
// BUG: Go reflection provides no way to disambiguate two named types
// of the same name and within the same package,
// but declared within the namespace of different functions.
// Named type.
if t.Name() != "" {
if qualified && t.PkgPath() != "" {
b = append(b, '"')
b = append(b, t.PkgPath()...)
b = append(b, '"')
b = append(b, '.')
b = append(b, t.Name()...)
} else {
b = append(b, t.String()...)
}
return b
}
// Unnamed type.
switch k := t.Kind(); k {
case reflect.Bool, reflect.String, reflect.UnsafePointer,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
b = append(b, k.String()...)
case reflect.Chan:
if t.ChanDir() == reflect.RecvDir {
b = append(b, "<-"...)
}
b = append(b, "chan"...)
if t.ChanDir() == reflect.SendDir {
b = append(b, "<-"...)
}
b = append(b, ' ')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Func:
if !elideFunc {
b = append(b, "func"...)
}
b = append(b, '(')
for i := 0; i < t.NumIn(); i++ {
if i > 0 {
b = append(b, ", "...)
}
if i == t.NumIn()-1 && t.IsVariadic() {
b = append(b, "..."...)
b = appendTypeName(b, t.In(i).Elem(), qualified, false)
} else {
b = appendTypeName(b, t.In(i), qualified, false)
}
}
b = append(b, ')')
switch t.NumOut() {
case 0:
// Do nothing
case 1:
b = append(b, ' ')
b = appendTypeName(b, t.Out(0), qualified, false)
default:
b = append(b, " ("...)
for i := 0; i < t.NumOut(); i++ {
if i > 0 {
b = append(b, ", "...)
}
b = appendTypeName(b, t.Out(i), qualified, false)
}
b = append(b, ')')
}
case reflect.Struct:
b = append(b, "struct{ "...)
for i := 0; i < t.NumField(); i++ {
if i > 0 {
b = append(b, "; "...)
}
sf := t.Field(i)
if !sf.Anonymous {
if qualified && sf.PkgPath != "" {
b = append(b, '"')
b = append(b, sf.PkgPath...)
b = append(b, '"')
b = append(b, '.')
}
b = append(b, sf.Name...)
b = append(b, ' ')
}
b = appendTypeName(b, sf.Type, qualified, false)
if sf.Tag != "" {
b = append(b, ' ')
b = strconv.AppendQuote(b, string(sf.Tag))
}
}
if b[len(b)-1] == ' ' {
b = b[:len(b)-1]
} else {
b = append(b, ' ')
}
b = append(b, '}')
case reflect.Slice, reflect.Array:
b = append(b, '[')
if k == reflect.Array {
b = strconv.AppendUint(b, uint64(t.Len()), 10)
}
b = append(b, ']')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Map:
b = append(b, "map["...)
b = appendTypeName(b, t.Key(), qualified, false)
b = append(b, ']')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Ptr:
b = append(b, '*')
b = appendTypeName(b, t.Elem(), qualified, false)
case reflect.Interface:
b = append(b, "interface{ "...)
for i := 0; i < t.NumMethod(); i++ {
if i > 0 {
b = append(b, "; "...)
}
m := t.Method(i)
if qualified && m.PkgPath != "" {
b = append(b, '"')
b = append(b, m.PkgPath...)
b = append(b, '"')
b = append(b, '.')
}
b = append(b, m.Name...)
b = appendTypeName(b, m.Type, qualified, true)
}
if b[len(b)-1] == ' ' {
b = b[:len(b)-1]
} else {
b = append(b, ' ')
}
b = append(b, '}')
default:
panic("invalid kind: " + k.String())
}
return b
}

View file

@ -21,3 +21,13 @@ func PointerOf(v reflect.Value) Pointer {
// assumes that the GC implementation does not use a moving collector.
return Pointer{v.Pointer(), v.Type()}
}
// IsNil reports whether the pointer is nil.
func (p Pointer) IsNil() bool {
return p.p == 0
}
// Uintptr returns the pointer as a uintptr.
func (p Pointer) Uintptr() uintptr {
return p.p
}

View file

@ -24,3 +24,13 @@ func PointerOf(v reflect.Value) Pointer {
// which is necessary if the GC ever uses a moving collector.
return Pointer{unsafe.Pointer(v.Pointer()), v.Type()}
}
// IsNil reports whether the pointer is nil.
func (p Pointer) IsNil() bool {
return p.p == nil
}
// Uintptr returns the pointer as a uintptr.
func (p Pointer) Uintptr() uintptr {
return uintptr(p.p)
}

View file

@ -177,7 +177,8 @@ type structField struct {
// pvx, pvy, and field are only valid if unexported is true.
unexported bool
mayForce bool // Forcibly allow visibility
pvx, pvy reflect.Value // Parent values
paddr bool // Was parent addressable?
pvx, pvy reflect.Value // Parent values (always addressible)
field reflect.StructField // Field information
}
@ -189,8 +190,8 @@ func (sf StructField) Values() (vx, vy reflect.Value) {
// Forcibly obtain read-write access to an unexported struct field.
if sf.mayForce {
vx = retrieveUnexportedField(sf.pvx, sf.field)
vy = retrieveUnexportedField(sf.pvy, sf.field)
vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr)
vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr)
return vx, vy // CanInterface reports true
}
return sf.vx, sf.vy // CanInterface reports false

View file

@ -41,7 +41,10 @@ func (r *defaultReporter) String() string {
if r.root.NumDiff == 0 {
return ""
}
return formatOptions{}.FormatDiff(r.root).String()
ptrs := new(pointerReferences)
text := formatOptions{}.FormatDiff(r.root, ptrs)
resolveReferences(text)
return text.String()
}
func assert(ok bool) {

View file

@ -11,14 +11,6 @@ import (
"github.com/google/go-cmp/cmp/internal/value"
)
// TODO: Enforce limits?
// * Enforce maximum number of records to print per node?
// * Enforce maximum size in bytes allowed?
// * As a heuristic, use less verbosity for equal nodes than unequal nodes.
// TODO: Enforce unique outputs?
// * Avoid Stringer methods if it results in same output?
// * Print pointer address if outputs still equal?
// numContextRecords is the number of surrounding equal records to print.
const numContextRecords = 2
@ -71,24 +63,66 @@ func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
opts.TypeMode = t
return opts
}
func (opts formatOptions) WithVerbosity(level int) formatOptions {
opts.VerbosityLevel = level
opts.LimitVerbosity = true
return opts
}
func (opts formatOptions) verbosity() uint {
switch {
case opts.VerbosityLevel < 0:
return 0
case opts.VerbosityLevel > 16:
return 16 // some reasonable maximum to avoid shift overflow
default:
return uint(opts.VerbosityLevel)
}
}
const maxVerbosityPreset = 3
// verbosityPreset modifies the verbosity settings given an index
// between 0 and maxVerbosityPreset, inclusive.
func verbosityPreset(opts formatOptions, i int) formatOptions {
opts.VerbosityLevel = int(opts.verbosity()) + 2*i
if i > 0 {
opts.AvoidStringer = true
}
if i >= maxVerbosityPreset {
opts.PrintAddresses = true
opts.QualifiedNames = true
}
return opts
}
// FormatDiff converts a valueNode tree into a textNode tree, where the later
// is a textual representation of the differences detected in the former.
func (opts formatOptions) FormatDiff(v *valueNode) textNode {
func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {
if opts.DiffMode == diffIdentical {
opts = opts.WithVerbosity(1)
} else {
opts = opts.WithVerbosity(3)
}
// Check whether we have specialized formatting for this node.
// This is not necessary, but helpful for producing more readable outputs.
if opts.CanFormatDiffSlice(v) {
return opts.FormatDiffSlice(v)
}
var parentKind reflect.Kind
if v.parent != nil && v.parent.TransformerName == "" {
parentKind = v.parent.Type.Kind()
}
// For leaf nodes, format the value based on the reflect.Values alone.
if v.MaxDepth == 0 {
switch opts.DiffMode {
case diffUnknown, diffIdentical:
// Format Equal.
if v.NumDiff == 0 {
outx := opts.FormatValue(v.ValueX, visitedPointers{})
outy := opts.FormatValue(v.ValueY, visitedPointers{})
outx := opts.FormatValue(v.ValueX, parentKind, ptrs)
outy := opts.FormatValue(v.ValueY, parentKind, ptrs)
if v.NumIgnored > 0 && v.NumSame == 0 {
return textEllipsis
} else if outx.Len() < outy.Len() {
@ -101,8 +135,13 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode {
// Format unequal.
assert(opts.DiffMode == diffUnknown)
var list textList
outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, visitedPointers{})
outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, visitedPointers{})
outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)
outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs)
for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
opts2 := verbosityPreset(opts, i).WithTypeMode(elideType)
outx = opts2.FormatValue(v.ValueX, parentKind, ptrs)
outy = opts2.FormatValue(v.ValueY, parentKind, ptrs)
}
if outx != nil {
list = append(list, textRecord{Diff: '-', Value: outx})
}
@ -111,34 +150,57 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode {
}
return opts.WithTypeMode(emitType).FormatType(v.Type, list)
case diffRemoved:
return opts.FormatValue(v.ValueX, visitedPointers{})
return opts.FormatValue(v.ValueX, parentKind, ptrs)
case diffInserted:
return opts.FormatValue(v.ValueY, visitedPointers{})
return opts.FormatValue(v.ValueY, parentKind, ptrs)
default:
panic("invalid diff mode")
}
}
// Register slice element to support cycle detection.
if parentKind == reflect.Slice {
ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true)
defer ptrs.Pop()
defer func() { out = wrapTrunkReferences(ptrRefs, out) }()
}
// Descend into the child value node.
if v.TransformerName != "" {
out := opts.WithTypeMode(emitType).FormatDiff(v.Value)
out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"}
out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}
return opts.FormatType(v.Type, out)
} else {
switch k := v.Type.Kind(); k {
case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map:
return opts.FormatType(v.Type, opts.formatDiffList(v.Records, k))
case reflect.Struct, reflect.Array, reflect.Slice:
out = opts.formatDiffList(v.Records, k, ptrs)
out = opts.FormatType(v.Type, out)
case reflect.Map:
// Register map to support cycle detection.
ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
defer ptrs.Pop()
out = opts.formatDiffList(v.Records, k, ptrs)
out = wrapTrunkReferences(ptrRefs, out)
out = opts.FormatType(v.Type, out)
case reflect.Ptr:
return textWrap{"&", opts.FormatDiff(v.Value), ""}
// Register pointer to support cycle detection.
ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
defer ptrs.Pop()
out = opts.FormatDiff(v.Value, ptrs)
out = wrapTrunkReferences(ptrRefs, out)
out = &textWrap{Prefix: "&", Value: out}
case reflect.Interface:
return opts.WithTypeMode(emitType).FormatDiff(v.Value)
out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
default:
panic(fmt.Sprintf("%v cannot have children", k))
}
return out
}
}
func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) textNode {
func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {
// Derive record name based on the data structure kind.
var name string
var formatKey func(reflect.Value) string
@ -154,7 +216,17 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
case reflect.Map:
name = "entry"
opts = opts.WithTypeMode(elideType)
formatKey = formatMapKey
formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) }
}
maxLen := -1
if opts.LimitVerbosity {
if opts.DiffMode == diffIdentical {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
} else {
maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc...
}
opts.VerbosityLevel--
}
// Handle unification.
@ -163,6 +235,11 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
var list textList
var deferredEllipsis bool // Add final "..." to indicate records were dropped
for _, r := range recs {
if len(list) == maxLen {
deferredEllipsis = true
break
}
// Elide struct fields that are zero value.
if k == reflect.Struct {
var isZero bool
@ -186,23 +263,31 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
}
continue
}
if out := opts.FormatDiff(r.Value); out != nil {
if out := opts.FormatDiff(r.Value, ptrs); out != nil {
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
}
}
if deferredEllipsis {
list.AppendEllipsis(diffStats{})
}
return textWrap{"{", list, "}"}
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
case diffUnknown:
default:
panic("invalid diff mode")
}
// Handle differencing.
var numDiffs int
var list textList
var keys []reflect.Value // invariant: len(list) == len(keys)
groups := coalesceAdjacentRecords(name, recs)
maxGroup := diffStats{Name: name}
for i, ds := range groups {
if maxLen >= 0 && numDiffs >= maxLen {
maxGroup = maxGroup.Append(ds)
continue
}
// Handle equal records.
if ds.NumDiff() == 0 {
// Compute the number of leading and trailing records to print.
@ -226,16 +311,21 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
// Format the equal values.
for _, r := range recs[:numLo] {
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
}
if numEqual > numLo+numHi {
ds.NumIdentical -= numLo + numHi
list.AppendEllipsis(ds)
for len(keys) < len(list) {
keys = append(keys, reflect.Value{})
}
}
for _, r := range recs[numEqual-numHi : numEqual] {
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
}
recs = recs[numEqual:]
continue
@ -247,24 +337,70 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te
case opts.CanFormatDiffSlice(r.Value):
out := opts.FormatDiffSlice(r.Value)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
case r.Value.NumChildren == r.Value.MaxDepth:
outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value)
outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value)
outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
opts2 := verbosityPreset(opts, i)
outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
}
if outx != nil {
list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
keys = append(keys, r.Key)
}
if outy != nil {
list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
keys = append(keys, r.Key)
}
default:
out := opts.FormatDiff(r.Value)
out := opts.FormatDiff(r.Value, ptrs)
list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
keys = append(keys, r.Key)
}
}
recs = recs[ds.NumDiff():]
numDiffs += ds.NumDiff()
}
assert(len(recs) == 0)
return textWrap{"{", list, "}"}
if maxGroup.IsZero() {
assert(len(recs) == 0)
} else {
list.AppendEllipsis(maxGroup)
for len(keys) < len(list) {
keys = append(keys, reflect.Value{})
}
}
assert(len(list) == len(keys))
// For maps, the default formatting logic uses fmt.Stringer which may
// produce ambiguous output. Avoid calling String to disambiguate.
if k == reflect.Map {
var ambiguous bool
seenKeys := map[string]reflect.Value{}
for i, currKey := range keys {
if currKey.IsValid() {
strKey := list[i].Key
prevKey, seen := seenKeys[strKey]
if seen && prevKey.CanInterface() && currKey.CanInterface() {
ambiguous = prevKey.Interface() != currKey.Interface()
if ambiguous {
break
}
}
seenKeys[strKey] = currKey
}
}
if ambiguous {
for i, k := range keys {
if k.IsValid() {
list[i].Key = formatMapKey(k, true, ptrs)
}
}
}
}
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
}
// coalesceAdjacentRecords coalesces the list of records into groups of

View file

@ -0,0 +1,264 @@
// Copyright 2020, 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.md file.
package cmp
import (
"fmt"
"reflect"
"strings"
"github.com/google/go-cmp/cmp/internal/flags"
"github.com/google/go-cmp/cmp/internal/value"
)
const (
pointerDelimPrefix = "⟪"
pointerDelimSuffix = "⟫"
)
// formatPointer prints the address of the pointer.
func formatPointer(p value.Pointer, withDelims bool) string {
v := p.Uintptr()
if flags.Deterministic {
v = 0xdeadf00f // Only used for stable testing purposes
}
if withDelims {
return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix
}
return formatHex(uint64(v))
}
// pointerReferences is a stack of pointers visited so far.
type pointerReferences [][2]value.Pointer
func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) {
if deref && vx.IsValid() {
vx = vx.Addr()
}
if deref && vy.IsValid() {
vy = vy.Addr()
}
switch d {
case diffUnknown, diffIdentical:
pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)}
case diffRemoved:
pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}}
case diffInserted:
pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)}
}
*ps = append(*ps, pp)
return pp
}
func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) {
p = value.PointerOf(v)
for _, pp := range *ps {
if p == pp[0] || p == pp[1] {
return p, true
}
}
*ps = append(*ps, [2]value.Pointer{p, p})
return p, false
}
func (ps *pointerReferences) Pop() {
*ps = (*ps)[:len(*ps)-1]
}
// trunkReferences is metadata for a textNode indicating that the sub-tree
// represents the value for either pointer in a pair of references.
type trunkReferences struct{ pp [2]value.Pointer }
// trunkReference is metadata for a textNode indicating that the sub-tree
// represents the value for the given pointer reference.
type trunkReference struct{ p value.Pointer }
// leafReference is metadata for a textNode indicating that the value is
// truncated as it refers to another part of the tree (i.e., a trunk).
type leafReference struct{ p value.Pointer }
func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode {
switch {
case pp[0].IsNil():
return &textWrap{Value: s, Metadata: trunkReference{pp[1]}}
case pp[1].IsNil():
return &textWrap{Value: s, Metadata: trunkReference{pp[0]}}
case pp[0] == pp[1]:
return &textWrap{Value: s, Metadata: trunkReference{pp[0]}}
default:
return &textWrap{Value: s, Metadata: trunkReferences{pp}}
}
}
func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode {
var prefix string
if printAddress {
prefix = formatPointer(p, true)
}
return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}}
}
func makeLeafReference(p value.Pointer, printAddress bool) textNode {
out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"}
var prefix string
if printAddress {
prefix = formatPointer(p, true)
}
return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}}
}
// resolveReferences walks the textNode tree searching for any leaf reference
// metadata and resolves each against the corresponding trunk references.
// Since pointer addresses in memory are not particularly readable to the user,
// it replaces each pointer value with an arbitrary and unique reference ID.
func resolveReferences(s textNode) {
var walkNodes func(textNode, func(textNode))
walkNodes = func(s textNode, f func(textNode)) {
f(s)
switch s := s.(type) {
case *textWrap:
walkNodes(s.Value, f)
case textList:
for _, r := range s {
walkNodes(r.Value, f)
}
}
}
// Collect all trunks and leaves with reference metadata.
var trunks, leaves []*textWrap
walkNodes(s, func(s textNode) {
if s, ok := s.(*textWrap); ok {
switch s.Metadata.(type) {
case leafReference:
leaves = append(leaves, s)
case trunkReference, trunkReferences:
trunks = append(trunks, s)
}
}
})
// No leaf references to resolve.
if len(leaves) == 0 {
return
}
// Collect the set of all leaf references to resolve.
leafPtrs := make(map[value.Pointer]bool)
for _, leaf := range leaves {
leafPtrs[leaf.Metadata.(leafReference).p] = true
}
// Collect the set of trunk pointers that are always paired together.
// This allows us to assign a single ID to both pointers for brevity.
// If a pointer in a pair ever occurs by itself or as a different pair,
// then the pair is broken.
pairedTrunkPtrs := make(map[value.Pointer]value.Pointer)
unpair := func(p value.Pointer) {
if !pairedTrunkPtrs[p].IsNil() {
pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half
}
pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half
}
for _, trunk := range trunks {
switch p := trunk.Metadata.(type) {
case trunkReference:
unpair(p.p) // standalone pointer cannot be part of a pair
case trunkReferences:
p0, ok0 := pairedTrunkPtrs[p.pp[0]]
p1, ok1 := pairedTrunkPtrs[p.pp[1]]
switch {
case !ok0 && !ok1:
// Register the newly seen pair.
pairedTrunkPtrs[p.pp[0]] = p.pp[1]
pairedTrunkPtrs[p.pp[1]] = p.pp[0]
case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]:
// Exact pair already seen; do nothing.
default:
// Pair conflicts with some other pair; break all pairs.
unpair(p.pp[0])
unpair(p.pp[1])
}
}
}
// Correlate each pointer referenced by leaves to a unique identifier,
// and print the IDs for each trunk that matches those pointers.
var nextID uint
ptrIDs := make(map[value.Pointer]uint)
newID := func() uint {
id := nextID
nextID++
return id
}
for _, trunk := range trunks {
switch p := trunk.Metadata.(type) {
case trunkReference:
if print := leafPtrs[p.p]; print {
id, ok := ptrIDs[p.p]
if !ok {
id = newID()
ptrIDs[p.p] = id
}
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id))
}
case trunkReferences:
print0 := leafPtrs[p.pp[0]]
print1 := leafPtrs[p.pp[1]]
if print0 || print1 {
id0, ok0 := ptrIDs[p.pp[0]]
id1, ok1 := ptrIDs[p.pp[1]]
isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0]
if isPair {
var id uint
assert(ok0 == ok1) // must be seen together or not at all
if ok0 {
assert(id0 == id1) // must have the same ID
id = id0
} else {
id = newID()
ptrIDs[p.pp[0]] = id
ptrIDs[p.pp[1]] = id
}
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id))
} else {
if print0 && !ok0 {
id0 = newID()
ptrIDs[p.pp[0]] = id0
}
if print1 && !ok1 {
id1 = newID()
ptrIDs[p.pp[1]] = id1
}
switch {
case print0 && print1:
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1))
case print0:
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0))
case print1:
trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1))
}
}
}
}
}
// Update all leaf references with the unique identifier.
for _, leaf := range leaves {
if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok {
leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id))
}
}
}
func formatReference(id uint) string {
return fmt.Sprintf("ref#%d", id)
}
func updateReferencePrefix(prefix, ref string) string {
if prefix == "" {
return pointerDelimPrefix + ref + pointerDelimSuffix
}
suffix := strings.TrimPrefix(prefix, pointerDelimPrefix)
return pointerDelimPrefix + ref + ": " + suffix
}

View file

@ -10,8 +10,8 @@ import (
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/google/go-cmp/cmp/internal/flags"
"github.com/google/go-cmp/cmp/internal/value"
)
@ -20,14 +20,22 @@ type formatValueOptions struct {
// methods like error.Error or fmt.Stringer.String.
AvoidStringer bool
// ShallowPointers controls whether to avoid descending into pointers.
// Useful when printing map keys, where pointer comparison is performed
// on the pointer address rather than the pointed-at value.
ShallowPointers bool
// PrintAddresses controls whether to print the address of all pointers,
// slice elements, and maps.
PrintAddresses bool
// QualifiedNames controls whether FormatType uses the fully qualified name
// (including the full package path as opposed to just the package name).
QualifiedNames bool
// VerbosityLevel controls the amount of output to produce.
// A higher value produces more output. A value of zero or lower produces
// no output (represented using an ellipsis).
// If LimitVerbosity is false, then the level is treated as infinite.
VerbosityLevel int
// LimitVerbosity specifies that formatting should respect VerbosityLevel.
LimitVerbosity bool
}
// FormatType prints the type as if it were wrapping s.
@ -44,12 +52,15 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
default:
return s
}
if opts.DiffMode == diffIdentical {
return s // elide type for identical nodes
}
case elideType:
return s
}
// Determine the type label, applying special handling for unnamed types.
typeName := t.String()
typeName := value.TypeString(t, opts.QualifiedNames)
if t.Name() == "" {
// According to Go grammar, certain type literals contain symbols that
// do not strongly bind to the next lexicographical token (e.g., *T).
@ -57,39 +68,78 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
case reflect.Chan, reflect.Func, reflect.Ptr:
typeName = "(" + typeName + ")"
}
typeName = strings.Replace(typeName, "struct {", "struct{", -1)
typeName = strings.Replace(typeName, "interface {", "interface{", -1)
}
return &textWrap{Prefix: typeName, Value: wrapParens(s)}
}
// Avoid wrap the value in parenthesis if unnecessary.
if s, ok := s.(textWrap); ok {
hasParens := strings.HasPrefix(s.Prefix, "(") && strings.HasSuffix(s.Suffix, ")")
hasBraces := strings.HasPrefix(s.Prefix, "{") && strings.HasSuffix(s.Suffix, "}")
// wrapParens wraps s with a set of parenthesis, but avoids it if the
// wrapped node itself is already surrounded by a pair of parenthesis or braces.
// It handles unwrapping one level of pointer-reference nodes.
func wrapParens(s textNode) textNode {
var refNode *textWrap
if s2, ok := s.(*textWrap); ok {
// Unwrap a single pointer reference node.
switch s2.Metadata.(type) {
case leafReference, trunkReference, trunkReferences:
refNode = s2
if s3, ok := refNode.Value.(*textWrap); ok {
s2 = s3
}
}
// Already has delimiters that make parenthesis unnecessary.
hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")")
hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}")
if hasParens || hasBraces {
return textWrap{typeName, s, ""}
return s
}
}
return textWrap{typeName + "(", s, ")"}
if refNode != nil {
refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"}
return s
}
return &textWrap{Prefix: "(", Value: s, Suffix: ")"}
}
// FormatValue prints the reflect.Value, taking extra care to avoid descending
// into pointers already in m. As pointers are visited, m is also updated.
func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out textNode) {
// into pointers already in ptrs. As pointers are visited, ptrs is also updated.
func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) {
if !v.IsValid() {
return nil
}
t := v.Type()
// Check slice element for cycles.
if parentKind == reflect.Slice {
ptrRef, visited := ptrs.Push(v.Addr())
if visited {
return makeLeafReference(ptrRef, false)
}
defer ptrs.Pop()
defer func() { out = wrapTrunkReference(ptrRef, false, out) }()
}
// Check whether there is an Error or String method to call.
if !opts.AvoidStringer && v.CanInterface() {
// Avoid calling Error or String methods on nil receivers since many
// implementations crash when doing so.
if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() {
var prefix, strVal string
switch v := v.Interface().(type) {
case error:
return textLine("e" + formatString(v.Error()))
prefix, strVal = "e", v.Error()
case fmt.Stringer:
return textLine("s" + formatString(v.String()))
prefix, strVal = "s", v.String()
}
if prefix != "" {
maxLen := len(strVal)
if opts.LimitVerbosity {
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
}
if len(strVal) > maxLen+len(textEllipsis) {
return textLine(prefix + formatString(strVal[:maxLen]) + string(textEllipsis))
}
return textLine(prefix + formatString(strVal))
}
}
}
@ -102,94 +152,136 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t
}
}()
var ptr string
switch t.Kind() {
case reflect.Bool:
return textLine(fmt.Sprint(v.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return textLine(fmt.Sprint(v.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
// Unnamed uints are usually bytes or words, so use hexadecimal.
if t.PkgPath() == "" || t.Kind() == reflect.Uintptr {
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return textLine(fmt.Sprint(v.Uint()))
case reflect.Uint8:
if parentKind == reflect.Slice || parentKind == reflect.Array {
return textLine(formatHex(v.Uint()))
}
return textLine(fmt.Sprint(v.Uint()))
case reflect.Uintptr:
return textLine(formatHex(v.Uint()))
case reflect.Float32, reflect.Float64:
return textLine(fmt.Sprint(v.Float()))
case reflect.Complex64, reflect.Complex128:
return textLine(fmt.Sprint(v.Complex()))
case reflect.String:
maxLen := v.Len()
if opts.LimitVerbosity {
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
}
if v.Len() > maxLen+len(textEllipsis) {
return textLine(formatString(v.String()[:maxLen]) + string(textEllipsis))
}
return textLine(formatString(v.String()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
return textLine(formatPointer(v))
return textLine(formatPointer(value.PointerOf(v), true))
case reflect.Struct:
var list textList
v := makeAddressable(v) // needed for retrieveUnexportedField
maxLen := v.NumField()
if opts.LimitVerbosity {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
opts.VerbosityLevel--
}
for i := 0; i < v.NumField(); i++ {
vv := v.Field(i)
if value.IsZero(vv) {
continue // Elide fields with zero values
}
s := opts.WithTypeMode(autoType).FormatValue(vv, m)
list = append(list, textRecord{Key: t.Field(i).Name, Value: s})
if len(list) == maxLen {
list.AppendEllipsis(diffStats{})
break
}
sf := t.Field(i)
if supportExporters && !isExported(sf.Name) {
vv = retrieveUnexportedField(v, sf, true)
}
s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs)
list = append(list, textRecord{Key: sf.Name, Value: s})
}
return textWrap{"{", list, "}"}
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
case reflect.Slice:
if v.IsNil() {
return textNil
}
if opts.PrintAddresses {
ptr = formatPointer(v)
}
fallthrough
case reflect.Array:
maxLen := v.Len()
if opts.LimitVerbosity {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
opts.VerbosityLevel--
}
var list textList
for i := 0; i < v.Len(); i++ {
vi := v.Index(i)
if vi.CanAddr() { // Check for cyclic elements
p := vi.Addr()
if m.Visit(p) {
var out textNode
out = textLine(formatPointer(p))
out = opts.WithTypeMode(emitType).FormatType(p.Type(), out)
out = textWrap{"*", out, ""}
list = append(list, textRecord{Value: out})
continue
}
if len(list) == maxLen {
list.AppendEllipsis(diffStats{})
break
}
s := opts.WithTypeMode(elideType).FormatValue(vi, m)
s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs)
list = append(list, textRecord{Value: s})
}
return textWrap{ptr + "{", list, "}"}
out = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
if t.Kind() == reflect.Slice && opts.PrintAddresses {
header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap())
out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out}
}
return out
case reflect.Map:
if v.IsNil() {
return textNil
}
if m.Visit(v) {
return textLine(formatPointer(v))
}
// Check pointer for cycles.
ptrRef, visited := ptrs.Push(v)
if visited {
return makeLeafReference(ptrRef, opts.PrintAddresses)
}
defer ptrs.Pop()
maxLen := v.Len()
if opts.LimitVerbosity {
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
opts.VerbosityLevel--
}
var list textList
for _, k := range value.SortKeys(v.MapKeys()) {
sk := formatMapKey(k)
sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), m)
if len(list) == maxLen {
list.AppendEllipsis(diffStats{})
break
}
sk := formatMapKey(k, false, ptrs)
sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs)
list = append(list, textRecord{Key: sk, Value: sv})
}
if opts.PrintAddresses {
ptr = formatPointer(v)
}
return textWrap{ptr + "{", list, "}"}
out = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
return out
case reflect.Ptr:
if v.IsNil() {
return textNil
}
if m.Visit(v) || opts.ShallowPointers {
return textLine(formatPointer(v))
}
if opts.PrintAddresses {
ptr = formatPointer(v)
// Check pointer for cycles.
ptrRef, visited := ptrs.Push(v)
if visited {
out = makeLeafReference(ptrRef, opts.PrintAddresses)
return &textWrap{Prefix: "&", Value: out}
}
defer ptrs.Pop()
skipType = true // Let the underlying value print the type instead
return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), m), ""}
out = opts.FormatValue(v.Elem(), t.Kind(), ptrs)
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
out = &textWrap{Prefix: "&", Value: out}
return out
case reflect.Interface:
if v.IsNil() {
return textNil
@ -197,7 +289,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t
// Interfaces accept different concrete types,
// so configure the underlying value to explicitly print the type.
skipType = true // Print the concrete type instead
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), m)
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs)
default:
panic(fmt.Sprintf("%v kind not handled", v.Kind()))
}
@ -205,11 +297,14 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t
// formatMapKey formats v as if it were a map key.
// The result is guaranteed to be a single line.
func formatMapKey(v reflect.Value) string {
func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string {
var opts formatOptions
opts.DiffMode = diffIdentical
opts.TypeMode = elideType
opts.ShallowPointers = true
s := opts.FormatValue(v, visitedPointers{}).String()
opts.PrintAddresses = disambiguate
opts.AvoidStringer = disambiguate
opts.QualifiedNames = disambiguate
s := opts.FormatValue(v, reflect.Map, ptrs).String()
return strings.TrimSpace(s)
}
@ -227,7 +322,7 @@ func formatString(s string) string {
rawInvalid := func(r rune) bool {
return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t')
}
if strings.IndexFunc(s, rawInvalid) < 0 {
if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 {
return "`" + s + "`"
}
return qs
@ -256,23 +351,3 @@ func formatHex(u uint64) string {
}
return fmt.Sprintf(f, u)
}
// formatPointer prints the address of the pointer.
func formatPointer(v reflect.Value) string {
p := v.Pointer()
if flags.Deterministic {
p = 0xdeadf00f // Only used for stable testing purposes
}
return fmt.Sprintf("⟪0x%x⟫", p)
}
type visitedPointers map[value.Pointer]struct{}
// Visit inserts pointer v into the visited map and reports whether it had
// already been visited before.
func (m visitedPointers) Visit(v reflect.Value) bool {
p := value.PointerOf(v)
_, visited := m[p]
m[p] = struct{}{}
return visited
}

View file

@ -8,6 +8,7 @@ import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
"unicode"
"unicode/utf8"
@ -23,11 +24,25 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
return false // Must be formatting in diff mode
case v.NumDiff == 0:
return false // No differences detected
case v.NumIgnored+v.NumCompared+v.NumTransformed > 0:
// TODO: Handle the case where someone uses bytes.Equal on a large slice.
return false // Some custom option was used to determined equality
case !v.ValueX.IsValid() || !v.ValueY.IsValid():
return false // Both values must be valid
case v.Type.Kind() == reflect.Slice && (v.ValueX.Len() == 0 || v.ValueY.Len() == 0):
return false // Both slice values have to be non-empty
case v.NumIgnored > 0:
return false // Some ignore option was used
case v.NumTransformed > 0:
return false // Some transform option was used
case v.NumCompared > 1:
return false // More than one comparison was used
case v.NumCompared == 1 && v.Type.Name() != "":
// The need for cmp to check applicability of options on every element
// in a slice is a significant performance detriment for large []byte.
// The workaround is to specify Comparer(bytes.Equal),
// which enables cmp to compare []byte more efficiently.
// If they differ, we still want to provide batched diffing.
// The logic disallows named types since they tend to have their own
// String method, with nicer formatting than what this provides.
return false
}
switch t := v.Type; t.Kind() {
@ -82,7 +97,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
}
if isText || isBinary {
var numLines, lastLineIdx, maxLineLen int
isBinary = false
isBinary = !utf8.ValidString(sx) || !utf8.ValidString(sy)
for i, r := range sx + sy {
if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError {
isBinary = true
@ -97,7 +112,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
}
}
isText = !isBinary
isLinedText = isText && numLines >= 4 && maxLineLen <= 256
isLinedText = isText && numLines >= 4 && maxLineLen <= 1024
}
// Format the string into printable records.
@ -117,6 +132,83 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
},
)
delim = "\n"
// If possible, use a custom triple-quote (""") syntax for printing
// differences in a string literal. This format is more readable,
// but has edge-cases where differences are visually indistinguishable.
// This format is avoided under the following conditions:
// • A line starts with `"""`
// • A line starts with "..."
// • A line contains non-printable characters
// • Adjacent different lines differ only by whitespace
//
// For example:
// """
// ... // 3 identical lines
// foo
// bar
// - baz
// + BAZ
// """
isTripleQuoted := true
prevRemoveLines := map[string]bool{}
prevInsertLines := map[string]bool{}
var list2 textList
list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true})
for _, r := range list {
if !r.Value.Equal(textEllipsis) {
line, _ := strconv.Unquote(string(r.Value.(textLine)))
line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
normLine := strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return -1 // drop whitespace to avoid visually indistinguishable output
}
return r
}, line)
isPrintable := func(r rune) bool {
return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
}
isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == ""
switch r.Diff {
case diffRemoved:
isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine]
prevRemoveLines[normLine] = true
case diffInserted:
isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine]
prevInsertLines[normLine] = true
}
if !isTripleQuoted {
break
}
r.Value = textLine(line)
r.ElideComma = true
}
if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group
prevRemoveLines = map[string]bool{}
prevInsertLines = map[string]bool{}
}
list2 = append(list2, r)
}
if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 {
list2 = list2[:len(list2)-1] // elide single empty line at the end
}
list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true})
if isTripleQuoted {
var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"}
switch t.Kind() {
case reflect.String:
if t != reflect.TypeOf(string("")) {
out = opts.FormatType(t, out)
}
case reflect.Slice:
// Always emit type for slices since the triple-quote syntax
// looks like a string (not a slice).
opts = opts.WithTypeMode(emitType)
out = opts.FormatType(t, out)
}
return out
}
// If the text appears to be single-lined text,
// then perform differencing in approximately fixed-sized chunks.
// The output is printed as quoted strings.
@ -129,6 +221,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
},
)
delim = ""
// If the text appears to be binary data,
// then perform differencing in approximately fixed-sized chunks.
// The output is inspired by hexdump.
@ -145,6 +238,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
return textRecord{Diff: d, Value: textLine(s), Comment: comment}
},
)
// For all other slices of primitive types,
// then perform differencing in approximately fixed-sized chunks.
// The size of each chunk depends on the width of the element kind.
@ -172,7 +266,9 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
switch t.Elem().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
ss = append(ss, fmt.Sprint(v.Index(i).Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
ss = append(ss, fmt.Sprint(v.Index(i).Uint()))
case reflect.Uint8, reflect.Uintptr:
ss = append(ss, formatHex(v.Index(i).Uint()))
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
ss = append(ss, fmt.Sprint(v.Index(i).Interface()))
@ -185,7 +281,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
}
// Wrap the output with appropriate type information.
var out textNode = textWrap{"{", list, "}"}
var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
if !isText {
// The "{...}" byte-sequence literal is not valid Go syntax for strings.
// Emit the type for extra clarity (e.g. "string{...}").
@ -196,12 +292,12 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
}
switch t.Kind() {
case reflect.String:
out = textWrap{"strings.Join(", out, fmt.Sprintf(", %q)", delim)}
out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
if t != reflect.TypeOf(string("")) {
out = opts.FormatType(t, out)
}
case reflect.Slice:
out = textWrap{"bytes.Join(", out, fmt.Sprintf(", %q)", delim)}
out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
if t != reflect.TypeOf([]byte(nil)) {
out = opts.FormatType(t, out)
}
@ -242,9 +338,22 @@ func (opts formatOptions) formatDiffSlice(
return n0 - v.Len()
}
var numDiffs int
maxLen := -1
if opts.LimitVerbosity {
maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc...
opts.VerbosityLevel--
}
groups := coalesceAdjacentEdits(name, es)
groups = coalesceInterveningIdentical(groups, chunkSize/4)
maxGroup := diffStats{Name: name}
for i, ds := range groups {
if maxLen >= 0 && numDiffs >= maxLen {
maxGroup = maxGroup.Append(ds)
continue
}
// Print equal.
if ds.NumDiff() == 0 {
// Compute the number of leading and trailing equal bytes to print.
@ -273,12 +382,18 @@ func (opts formatOptions) formatDiffSlice(
}
// Print unequal.
len0 := len(list)
nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved)
vx = vx.Slice(nx, vx.Len())
ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted)
vy = vy.Slice(ny, vy.Len())
numDiffs += len(list) - len0
}
if maxGroup.IsZero() {
assert(vx.Len() == 0 && vy.Len() == 0)
} else {
list.AppendEllipsis(maxGroup)
}
assert(vx.Len() == 0 && vy.Len() == 0)
return list
}

View file

@ -10,12 +10,15 @@ import (
"math/rand"
"strings"
"time"
"unicode/utf8"
"github.com/google/go-cmp/cmp/internal/flags"
)
var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
const maxColumnLength = 80
type indentMode int
func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
@ -91,21 +94,22 @@ type textNode interface {
// textWrap is a wrapper that concatenates a prefix and/or a suffix
// to the underlying node.
type textWrap struct {
Prefix string // e.g., "bytes.Buffer{"
Value textNode // textWrap | textList | textLine
Suffix string // e.g., "}"
Prefix string // e.g., "bytes.Buffer{"
Value textNode // textWrap | textList | textLine
Suffix string // e.g., "}"
Metadata interface{} // arbitrary metadata; has no effect on formatting
}
func (s textWrap) Len() int {
func (s *textWrap) Len() int {
return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
}
func (s1 textWrap) Equal(s2 textNode) bool {
if s2, ok := s2.(textWrap); ok {
func (s1 *textWrap) Equal(s2 textNode) bool {
if s2, ok := s2.(*textWrap); ok {
return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
}
return false
}
func (s textWrap) String() string {
func (s *textWrap) String() string {
var d diffMode
var n indentMode
_, s2 := s.formatCompactTo(nil, d)
@ -114,7 +118,7 @@ func (s textWrap) String() string {
b = append(b, '\n') // Trailing newline
return string(b)
}
func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
n0 := len(b) // Original buffer length
b = append(b, s.Prefix...)
b, s.Value = s.Value.formatCompactTo(b, d)
@ -124,7 +128,7 @@ func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
}
return b, s
}
func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
b = append(b, s.Prefix...)
b = s.Value.formatExpandedTo(b, d, n)
b = append(b, s.Suffix...)
@ -136,22 +140,23 @@ func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
// of the textList.formatCompactTo method.
type textList []textRecord
type textRecord struct {
Diff diffMode // e.g., 0 or '-' or '+'
Key string // e.g., "MyField"
Value textNode // textWrap | textLine
Comment fmt.Stringer // e.g., "6 identical fields"
Diff diffMode // e.g., 0 or '-' or '+'
Key string // e.g., "MyField"
Value textNode // textWrap | textLine
ElideComma bool // avoid trailing comma
Comment fmt.Stringer // e.g., "6 identical fields"
}
// AppendEllipsis appends a new ellipsis node to the list if none already
// exists at the end. If cs is non-zero it coalesces the statistics with the
// previous diffStats.
func (s *textList) AppendEllipsis(ds diffStats) {
hasStats := ds != diffStats{}
hasStats := !ds.IsZero()
if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
if hasStats {
*s = append(*s, textRecord{Value: textEllipsis, Comment: ds})
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds})
} else {
*s = append(*s, textRecord{Value: textEllipsis})
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true})
}
return
}
@ -191,7 +196,7 @@ func (s1 textList) Equal(s2 textNode) bool {
}
func (s textList) String() string {
return textWrap{"{", s, "}"}.String()
return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String()
}
func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
@ -221,7 +226,7 @@ func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
}
// Force multi-lined output when printing a removed/inserted node that
// is sufficiently long.
if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > 80 {
if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength {
multiLine = true
}
if !multiLine {
@ -236,16 +241,50 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
_, isLine := r.Value.(textLine)
return r.Key == "" || !isLine
},
func(r textRecord) int { return len(r.Key) },
func(r textRecord) int { return utf8.RuneCountInString(r.Key) },
)
alignValueLens := s.alignLens(
func(r textRecord) bool {
_, isLine := r.Value.(textLine)
return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
},
func(r textRecord) int { return len(r.Value.(textLine)) },
func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) },
)
// Format lists of simple lists in a batched form.
// If the list is sequence of only textLine values,
// then batch multiple values on a single line.
var isSimple bool
for _, r := range s {
_, isLine := r.Value.(textLine)
isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil
if !isSimple {
break
}
}
if isSimple {
n++
var batch []byte
emitBatch := func() {
if len(batch) > 0 {
b = n.appendIndent(append(b, '\n'), d)
b = append(b, bytes.TrimRight(batch, " ")...)
batch = batch[:0]
}
}
for _, r := range s {
line := r.Value.(textLine)
if len(batch)+len(line)+len(", ") > maxColumnLength {
emitBatch()
}
batch = append(batch, line...)
batch = append(batch, ", "...)
}
emitBatch()
n--
return n.appendIndent(append(b, '\n'), d)
}
// Format the list as a multi-lined output.
n++
for i, r := range s {
@ -256,7 +295,7 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
b = alignKeyLens[i].appendChar(b, ' ')
b = r.Value.formatExpandedTo(b, d|r.Diff, n)
if !r.Value.Equal(textEllipsis) {
if !r.ElideComma {
b = append(b, ',')
}
b = alignValueLens[i].appendChar(b, ' ')
@ -332,6 +371,11 @@ type diffStats struct {
NumModified int
}
func (s diffStats) IsZero() bool {
s.Name = ""
return s == diffStats{}
}
func (s diffStats) NumDiff() int {
return s.NumRemoved + s.NumInserted + s.NumModified
}

21
vendor/github.com/hetznercloud/hcloud-go/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-2020 Hetzner Cloud GmbH
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.

View file

@ -0,0 +1,210 @@
package hcloud
import (
"context"
"fmt"
"net/url"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Action represents an action in the Hetzner Cloud.
type Action struct {
ID int
Status ActionStatus
Command string
Progress int
Started time.Time
Finished time.Time
ErrorCode string
ErrorMessage string
Resources []*ActionResource
}
// ActionStatus represents an action's status.
type ActionStatus string
// List of action statuses.
const (
ActionStatusRunning ActionStatus = "running"
ActionStatusSuccess ActionStatus = "success"
ActionStatusError ActionStatus = "error"
)
// ActionResource references other resources from an action.
type ActionResource struct {
ID int
Type ActionResourceType
}
// ActionResourceType represents an action's resource reference type.
type ActionResourceType string
// List of action resource reference types.
const (
ActionResourceTypeServer ActionResourceType = "server"
ActionResourceTypeImage ActionResourceType = "image"
ActionResourceTypeISO ActionResourceType = "iso"
ActionResourceTypeFloatingIP ActionResourceType = "floating_ip"
ActionResourceTypeVolume ActionResourceType = "volume"
)
// ActionError is the error of an action.
type ActionError struct {
Code string
Message string
}
func (e ActionError) Error() string {
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
}
func (a *Action) Error() error {
if a.ErrorCode != "" && a.ErrorMessage != "" {
return ActionError{
Code: a.ErrorCode,
Message: a.ErrorMessage,
}
}
return nil
}
// ActionClient is a client for the actions API.
type ActionClient struct {
client *Client
}
// GetByID retrieves an action by its ID. If the action does not exist, nil is returned.
func (c *ActionClient) GetByID(ctx context.Context, id int) (*Action, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/actions/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.ActionGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return ActionFromSchema(body.Action), resp, nil
}
// ActionListOpts specifies options for listing actions.
type ActionListOpts struct {
ListOpts
Status []ActionStatus
Sort []string
}
func (l ActionListOpts) values() url.Values {
vals := l.ListOpts.values()
for _, status := range l.Status {
vals.Add("status", string(status))
}
for _, sort := range l.Sort {
vals.Add("sort", sort)
}
return vals
}
// List returns a list of actions for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *ActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) {
path := "/actions?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.ActionListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
actions := make([]*Action, 0, len(body.Actions))
for _, i := range body.Actions {
actions = append(actions, ActionFromSchema(i))
}
return actions, resp, nil
}
// All returns all actions.
func (c *ActionClient) All(ctx context.Context) ([]*Action, error) {
allActions := []*Action{}
opts := ActionListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
actions, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allActions = append(allActions, actions...)
return resp, nil
})
if err != nil {
return nil, err
}
return allActions, nil
}
// WatchProgress watches the action's progress until it completes with success or error.
func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) {
errCh := make(chan error, 1)
progressCh := make(chan int)
go func() {
defer close(errCh)
defer close(progressCh)
ticker := time.NewTicker(c.client.pollInterval)
sendProgress := func(p int) {
select {
case progressCh <- p:
break
default:
break
}
}
for {
select {
case <-ctx.Done():
errCh <- ctx.Err()
return
case <-ticker.C:
break
}
a, _, err := c.GetByID(ctx, action.ID)
if err != nil {
errCh <- err
return
}
switch a.Status {
case ActionStatusRunning:
sendProgress(a.Progress)
break
case ActionStatusSuccess:
sendProgress(100)
errCh <- nil
return
case ActionStatusError:
errCh <- a.Error()
return
}
}
}()
return progressCh, errCh
}

View file

@ -0,0 +1,246 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Certificate represents an certificate in the Hetzner Cloud.
type Certificate struct {
ID int
Name string
Labels map[string]string
Certificate string
Created time.Time
NotValidBefore time.Time
NotValidAfter time.Time
DomainNames []string
Fingerprint string
}
// CertificateClient is a client for the Certificates API.
type CertificateClient struct {
client *Client
}
// GetByID retrieves a Certificate by its ID. If the Certificate does not exist, nil is returned.
func (c *CertificateClient) GetByID(ctx context.Context, id int) (*Certificate, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/certificates/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.CertificateGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return CertificateFromSchema(body.Certificate), resp, nil
}
// GetByName retrieves a Certificate by its name. If the Certificate does not exist, nil is returned.
func (c *CertificateClient) GetByName(ctx context.Context, name string) (*Certificate, *Response, error) {
if name == "" {
return nil, nil, nil
}
Certificate, response, err := c.List(ctx, CertificateListOpts{Name: name})
if len(Certificate) == 0 {
return nil, response, err
}
return Certificate[0], response, err
}
// Get retrieves a Certificate by its ID if the input can be parsed as an integer, otherwise it
// retrieves a Certificate by its name. If the Certificate does not exist, nil is returned.
func (c *CertificateClient) Get(ctx context.Context, idOrName string) (*Certificate, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// CertificateListOpts specifies options for listing Certificates.
type CertificateListOpts struct {
ListOpts
Name string
}
func (l CertificateListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of Certificates for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *CertificateClient) List(ctx context.Context, opts CertificateListOpts) ([]*Certificate, *Response, error) {
path := "/certificates?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.CertificateListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
Certificates := make([]*Certificate, 0, len(body.Certificates))
for _, s := range body.Certificates {
Certificates = append(Certificates, CertificateFromSchema(s))
}
return Certificates, resp, nil
}
// All returns all Certificates.
func (c *CertificateClient) All(ctx context.Context) ([]*Certificate, error) {
allCertificates := []*Certificate{}
opts := CertificateListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
Certificate, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allCertificates = append(allCertificates, Certificate...)
return resp, nil
})
if err != nil {
return nil, err
}
return allCertificates, nil
}
// AllWithOpts returns all Certificates for the given options.
func (c *CertificateClient) AllWithOpts(ctx context.Context, opts CertificateListOpts) ([]*Certificate, error) {
var allCertificates []*Certificate
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
Certificates, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allCertificates = append(allCertificates, Certificates...)
return resp, nil
})
if err != nil {
return nil, err
}
return allCertificates, nil
}
// CertificateCreateOpts specifies options for creating a new Certificate.
type CertificateCreateOpts struct {
Name string
Certificate string
PrivateKey string
Labels map[string]string
}
// Validate checks if options are valid.
func (o CertificateCreateOpts) Validate() error {
if o.Name == "" {
return errors.New("missing name")
}
if o.Certificate == "" {
return errors.New("missing certificate")
}
if o.PrivateKey == "" {
return errors.New("missing private key")
}
return nil
}
// Create creates a new certificate.
func (c *CertificateClient) Create(ctx context.Context, opts CertificateCreateOpts) (*Certificate, *Response, error) {
if err := opts.Validate(); err != nil {
return nil, nil, err
}
reqBody := schema.CertificateCreateRequest{
Name: opts.Name,
Certificate: opts.Certificate,
PrivateKey: opts.PrivateKey,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/certificates", bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.CertificateCreateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return CertificateFromSchema(respBody.Certificate), resp, nil
}
// CertificateUpdateOpts specifies options for updating a Certificate.
type CertificateUpdateOpts struct {
Name string
Labels map[string]string
}
// Update updates a Certificate.
func (c *CertificateClient) Update(ctx context.Context, certificate *Certificate, opts CertificateUpdateOpts) (*Certificate, *Response, error) {
reqBody := schema.CertificateUpdateRequest{}
if opts.Name != "" {
reqBody.Name = &opts.Name
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/certificates/%d", certificate.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.CertificateUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return CertificateFromSchema(respBody.Certificate), resp, nil
}
// Delete deletes a certificate.
func (c *CertificateClient) Delete(ctx context.Context, certificate *Certificate) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/certificates/%d", certificate.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}

View file

@ -0,0 +1,394 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Endpoint is the base URL of the API.
const Endpoint = "https://api.hetzner.cloud/v1"
// UserAgent is the value for the library part of the User-Agent header
// that is sent with each request.
const UserAgent = "hcloud-go/" + Version
// A BackoffFunc returns the duration to wait before performing the
// next retry. The retries argument specifies how many retries have
// already been performed. When called for the first time, retries is 0.
type BackoffFunc func(retries int) time.Duration
// ConstantBackoff returns a BackoffFunc which backs off for
// constant duration d.
func ConstantBackoff(d time.Duration) BackoffFunc {
return func(_ int) time.Duration {
return d
}
}
// ExponentialBackoff returns a BackoffFunc which implements an exponential
// backoff using the formula: b^retries * d
func ExponentialBackoff(b float64, d time.Duration) BackoffFunc {
return func(retries int) time.Duration {
return time.Duration(math.Pow(b, float64(retries))) * d
}
}
// Client is a client for the Hetzner Cloud API.
type Client struct {
endpoint string
token string
pollInterval time.Duration
backoffFunc BackoffFunc
httpClient *http.Client
applicationName string
applicationVersion string
userAgent string
debugWriter io.Writer
Action ActionClient
Certificate CertificateClient
Datacenter DatacenterClient
FloatingIP FloatingIPClient
Image ImageClient
ISO ISOClient
LoadBalancer LoadBalancerClient
LoadBalancerType LoadBalancerTypeClient
Location LocationClient
Network NetworkClient
Pricing PricingClient
Server ServerClient
ServerType ServerTypeClient
SSHKey SSHKeyClient
Volume VolumeClient
}
// A ClientOption is used to configure a Client.
type ClientOption func(*Client)
// WithEndpoint configures a Client to use the specified API endpoint.
func WithEndpoint(endpoint string) ClientOption {
return func(client *Client) {
client.endpoint = strings.TrimRight(endpoint, "/")
}
}
// WithToken configures a Client to use the specified token for authentication.
func WithToken(token string) ClientOption {
return func(client *Client) {
client.token = token
}
}
// WithPollInterval configures a Client to use the specified interval when polling
// from the API.
func WithPollInterval(pollInterval time.Duration) ClientOption {
return func(client *Client) {
client.pollInterval = pollInterval
}
}
// WithBackoffFunc configures a Client to use the specified backoff function.
func WithBackoffFunc(f BackoffFunc) ClientOption {
return func(client *Client) {
client.backoffFunc = f
}
}
// WithApplication configures a Client with the given application name and
// application version. The version may be blank. Programs are encouraged
// to at least set an application name.
func WithApplication(name, version string) ClientOption {
return func(client *Client) {
client.applicationName = name
client.applicationVersion = version
}
}
// WithDebugWriter configures a Client to print debug information to the given
// writer. To, for example, print debug information on stderr, set it to os.Stderr.
func WithDebugWriter(debugWriter io.Writer) ClientOption {
return func(client *Client) {
client.debugWriter = debugWriter
}
}
// WithHTTPClient configures a Client to perform HTTP requests with httpClient.
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(client *Client) {
client.httpClient = httpClient
}
}
// NewClient creates a new client.
func NewClient(options ...ClientOption) *Client {
client := &Client{
endpoint: Endpoint,
httpClient: &http.Client{},
backoffFunc: ExponentialBackoff(2, 500*time.Millisecond),
pollInterval: 500 * time.Millisecond,
}
for _, option := range options {
option(client)
}
client.buildUserAgent()
client.Action = ActionClient{client: client}
client.Datacenter = DatacenterClient{client: client}
client.FloatingIP = FloatingIPClient{client: client}
client.Image = ImageClient{client: client}
client.ISO = ISOClient{client: client}
client.Location = LocationClient{client: client}
client.Network = NetworkClient{client: client}
client.Pricing = PricingClient{client: client}
client.Server = ServerClient{client: client}
client.ServerType = ServerTypeClient{client: client}
client.SSHKey = SSHKeyClient{client: client}
client.Volume = VolumeClient{client: client}
client.LoadBalancer = LoadBalancerClient{client: client}
client.LoadBalancerType = LoadBalancerTypeClient{client: client}
client.Certificate = CertificateClient{client: client}
return client
}
// NewRequest creates an HTTP request against the API. The returned request
// is assigned with ctx and has all necessary headers set (auth, user agent, etc.).
func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
url := c.endpoint + path
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", c.userAgent)
if c.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req = req.WithContext(ctx)
return req, nil
}
// Do performs an HTTP request against the API.
func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) {
var retries int
var body []byte
var err error
if r.ContentLength > 0 {
body, err = ioutil.ReadAll(r.Body)
if err != nil {
r.Body.Close()
return nil, err
}
r.Body.Close()
}
for {
if r.ContentLength > 0 {
r.Body = ioutil.NopCloser(bytes.NewReader(body))
}
if c.debugWriter != nil {
// To get the response body we need to read it before the request was actually send. https://github.com/golang/go/issues/29792
dumpReq, err := httputil.DumpRequestOut(r, true)
if err != nil {
return nil, err
}
fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq)
}
resp, err := c.httpClient.Do(r)
if err != nil {
return nil, err
}
response := &Response{Response: resp}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
resp.Body.Close()
return response, err
}
resp.Body.Close()
resp.Body = ioutil.NopCloser(bytes.NewReader(body))
if c.debugWriter != nil {
dumpResp, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, err
}
fmt.Fprintf(c.debugWriter, "--- Response:\n%s\n\n", dumpResp)
}
if err = response.readMeta(body); err != nil {
return response, fmt.Errorf("hcloud: error reading response meta data: %s", err)
}
if resp.StatusCode >= 400 && resp.StatusCode <= 599 {
err = errorFromResponse(resp, body)
if err == nil {
err = fmt.Errorf("hcloud: server responded with status code %d", resp.StatusCode)
} else {
if isRetryable(err) {
c.backoff(retries)
retries++
continue
}
}
return response, err
}
if v != nil {
if w, ok := v.(io.Writer); ok {
_, err = io.Copy(w, bytes.NewReader(body))
} else {
err = json.Unmarshal(body, v)
}
}
return response, err
}
}
func isRetryable(error error) bool {
err, ok := error.(Error)
if !ok {
return false
}
return err.Code == ErrorCodeRateLimitExceeded || err.Code == ErrorCodeConflict
}
func (c *Client) backoff(retries int) {
time.Sleep(c.backoffFunc(retries))
}
func (c *Client) all(f func(int) (*Response, error)) (*Response, error) {
var (
page = 1
)
for {
resp, err := f(page)
if err != nil {
return nil, err
}
if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 {
return resp, nil
}
page = resp.Meta.Pagination.NextPage
}
}
func (c *Client) buildUserAgent() {
switch {
case c.applicationName != "" && c.applicationVersion != "":
c.userAgent = c.applicationName + "/" + c.applicationVersion + " " + UserAgent
case c.applicationName != "" && c.applicationVersion == "":
c.userAgent = c.applicationName + " " + UserAgent
default:
c.userAgent = UserAgent
}
}
func errorFromResponse(resp *http.Response, body []byte) error {
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
return nil
}
var respBody schema.ErrorResponse
if err := json.Unmarshal(body, &respBody); err != nil {
return nil
}
if respBody.Error.Code == "" && respBody.Error.Message == "" {
return nil
}
return ErrorFromSchema(respBody.Error)
}
// Response represents a response from the API. It embeds http.Response.
type Response struct {
*http.Response
Meta Meta
}
func (r *Response) readMeta(body []byte) error {
if h := r.Header.Get("RateLimit-Limit"); h != "" {
r.Meta.Ratelimit.Limit, _ = strconv.Atoi(h)
}
if h := r.Header.Get("RateLimit-Remaining"); h != "" {
r.Meta.Ratelimit.Remaining, _ = strconv.Atoi(h)
}
if h := r.Header.Get("RateLimit-Reset"); h != "" {
if ts, err := strconv.ParseInt(h, 10, 64); err == nil {
r.Meta.Ratelimit.Reset = time.Unix(ts, 0)
}
}
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
var s schema.MetaResponse
if err := json.Unmarshal(body, &s); err != nil {
return err
}
if s.Meta.Pagination != nil {
p := PaginationFromSchema(*s.Meta.Pagination)
r.Meta.Pagination = &p
}
}
return nil
}
// Meta represents meta information included in an API response.
type Meta struct {
Pagination *Pagination
Ratelimit Ratelimit
}
// Pagination represents pagination meta information.
type Pagination struct {
Page int
PerPage int
PreviousPage int
NextPage int
LastPage int
TotalEntries int
}
// Ratelimit represents ratelimit information.
type Ratelimit struct {
Limit int
Remaining int
Reset time.Time
}
// ListOpts specifies options for listing resources.
type ListOpts struct {
Page int // Page (starting at 1)
PerPage int // Items per page (0 means default)
LabelSelector string // Label selector for filtering by labels
}
func (l ListOpts) values() url.Values {
vals := url.Values{}
if l.Page > 0 {
vals.Add("page", strconv.Itoa(l.Page))
}
if l.PerPage > 0 {
vals.Add("per_page", strconv.Itoa(l.PerPage))
}
if len(l.LabelSelector) > 0 {
vals.Add("label_selector", l.LabelSelector)
}
return vals
}

View file

@ -0,0 +1,129 @@
package hcloud
import (
"context"
"fmt"
"net/url"
"strconv"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Datacenter represents a datacenter in the Hetzner Cloud.
type Datacenter struct {
ID int
Name string
Description string
Location *Location
ServerTypes DatacenterServerTypes
}
// DatacenterServerTypes represents the server types available and supported in a datacenter.
type DatacenterServerTypes struct {
Supported []*ServerType
Available []*ServerType
}
// DatacenterClient is a client for the datacenter API.
type DatacenterClient struct {
client *Client
}
// GetByID retrieves a datacenter by its ID. If the datacenter does not exist, nil is returned.
func (c *DatacenterClient) GetByID(ctx context.Context, id int) (*Datacenter, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/datacenters/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.DatacenterGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, resp, err
}
return DatacenterFromSchema(body.Datacenter), resp, nil
}
// GetByName retrieves an datacenter by its name. If the datacenter does not exist, nil is returned.
func (c *DatacenterClient) GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) {
if name == "" {
return nil, nil, nil
}
datacenters, response, err := c.List(ctx, DatacenterListOpts{Name: name})
if len(datacenters) == 0 {
return nil, response, err
}
return datacenters[0], response, err
}
// Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it
// retrieves a datacenter by its name. If the datacenter does not exist, nil is returned.
func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// DatacenterListOpts specifies options for listing datacenters.
type DatacenterListOpts struct {
ListOpts
Name string
}
func (l DatacenterListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of datacenters for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *DatacenterClient) List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) {
path := "/datacenters?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.DatacenterListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
datacenters := make([]*Datacenter, 0, len(body.Datacenters))
for _, i := range body.Datacenters {
datacenters = append(datacenters, DatacenterFromSchema(i))
}
return datacenters, resp, nil
}
// All returns all datacenters.
func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) {
allDatacenters := []*Datacenter{}
opts := DatacenterListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
datacenters, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allDatacenters = append(allDatacenters, datacenters...)
return resp, nil
})
if err != nil {
return nil, err
}
return allDatacenters, nil
}

View file

@ -0,0 +1,62 @@
package hcloud
import "fmt"
// ErrorCode represents an error code returned from the API.
type ErrorCode string
// Error codes returned from the API.
const (
ErrorCodeServiceError ErrorCode = "service_error" // Generic service error
ErrorCodeRateLimitExceeded ErrorCode = "rate_limit_exceeded" // Rate limit exceeded
ErrorCodeUnknownError ErrorCode = "unknown_error" // Unknown error
ErrorCodeNotFound ErrorCode = "not_found" // Resource not found
ErrorCodeInvalidInput ErrorCode = "invalid_input" // Validation error
ErrorCodeForbidden ErrorCode = "forbidden" // Insufficient permissions
ErrorCodeJSONError ErrorCode = "json_error" // Invalid JSON in request
ErrorCodeLocked ErrorCode = "locked" // Item is locked (Another action is running)
ErrorCodeResourceLimitExceeded ErrorCode = "resource_limit_exceeded" // Resource limit exceeded
ErrorCodeResourceUnavailable ErrorCode = "resource_unavailable" // Resource currently unavailable
ErrorCodeUniquenessError ErrorCode = "uniqueness_error" // One or more fields must be unique
ErrorCodeProtected ErrorCode = "protected" // The actions you are trying is protected
ErrorCodeMaintenance ErrorCode = "maintenance" // Cannot perform operation due to maintenance
ErrorCodeConflict ErrorCode = "conflict" // The resource has changed during the request, please retry
ErrorCodeServerAlreadyAttached ErrorCode = "server_already_attached" // The server is already attached to the resource
// Deprecated error codes
// The actual value of this error code is limit_reached. The new error code
// rate_limit_exceeded for ratelimiting was introduced before Hetzner Cloud
// launched into the public. To make clients using the old error code still
// work as expected, we set the value of the old error code to that of the
// new error code.
ErrorCodeLimitReached = ErrorCodeRateLimitExceeded
)
// Error is an error returned from the API.
type Error struct {
Code ErrorCode
Message string
Details interface{}
}
func (e Error) Error() string {
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
}
// ErrorDetailsInvalidInput contains the details of an 'invalid_input' error.
type ErrorDetailsInvalidInput struct {
Fields []ErrorDetailsInvalidInputField
}
// ErrorDetailsInvalidInputField contains the validation errors reported on a field.
type ErrorDetailsInvalidInputField struct {
Name string
Messages []string
}
// IsError returns whether err is an API error with the given error code.
func IsError(err error, code ErrorCode) bool {
apiErr, ok := err.(Error)
return ok && apiErr.Code == code
}

View file

@ -0,0 +1,378 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// FloatingIP represents a Floating IP in the Hetzner Cloud.
type FloatingIP struct {
ID int
Description string
Created time.Time
IP net.IP
Network *net.IPNet
Type FloatingIPType
Server *Server
DNSPtr map[string]string
HomeLocation *Location
Blocked bool
Protection FloatingIPProtection
Labels map[string]string
Name string
}
// DNSPtrForIP returns the reverse DNS pointer of the IP address.
func (f *FloatingIP) DNSPtrForIP(ip net.IP) string {
return f.DNSPtr[ip.String()]
}
// FloatingIPProtection represents the protection level of a Floating IP.
type FloatingIPProtection struct {
Delete bool
}
// FloatingIPType represents the type of a Floating IP.
type FloatingIPType string
// Floating IP types.
const (
FloatingIPTypeIPv4 FloatingIPType = "ipv4"
FloatingIPTypeIPv6 FloatingIPType = "ipv6"
)
// FloatingIPClient is a client for the Floating IP API.
type FloatingIPClient struct {
client *Client
}
// GetByID retrieves a Floating IP by its ID. If the Floating IP does not exist,
// nil is returned.
func (c *FloatingIPClient) GetByID(ctx context.Context, id int) (*FloatingIP, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/floating_ips/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.FloatingIPGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, resp, err
}
return FloatingIPFromSchema(body.FloatingIP), resp, nil
}
// GetByName retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned.
func (c *FloatingIPClient) GetByName(ctx context.Context, name string) (*FloatingIP, *Response, error) {
if name == "" {
return nil, nil, nil
}
floatingIPs, response, err := c.List(ctx, FloatingIPListOpts{Name: name})
if len(floatingIPs) == 0 {
return nil, response, err
}
return floatingIPs[0], response, err
}
// Get retrieves a Floating IP by its ID if the input can be parsed as an integer, otherwise it
// retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned.
func (c *FloatingIPClient) Get(ctx context.Context, idOrName string) (*FloatingIP, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// FloatingIPListOpts specifies options for listing Floating IPs.
type FloatingIPListOpts struct {
ListOpts
Name string
}
func (l FloatingIPListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of Floating IPs for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *FloatingIPClient) List(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, *Response, error) {
path := "/floating_ips?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.FloatingIPListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
floatingIPs := make([]*FloatingIP, 0, len(body.FloatingIPs))
for _, s := range body.FloatingIPs {
floatingIPs = append(floatingIPs, FloatingIPFromSchema(s))
}
return floatingIPs, resp, nil
}
// All returns all Floating IPs.
func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) {
return c.AllWithOpts(ctx, FloatingIPListOpts{ListOpts: ListOpts{PerPage: 50}})
}
// AllWithOpts returns all Floating IPs for the given options.
func (c *FloatingIPClient) AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) {
allFloatingIPs := []*FloatingIP{}
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
floatingIPs, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allFloatingIPs = append(allFloatingIPs, floatingIPs...)
return resp, nil
})
if err != nil {
return nil, err
}
return allFloatingIPs, nil
}
// FloatingIPCreateOpts specifies options for creating a Floating IP.
type FloatingIPCreateOpts struct {
Type FloatingIPType
HomeLocation *Location
Server *Server
Description *string
Name *string
Labels map[string]string
}
// Validate checks if options are valid.
func (o FloatingIPCreateOpts) Validate() error {
switch o.Type {
case FloatingIPTypeIPv4, FloatingIPTypeIPv6:
break
default:
return errors.New("missing or invalid type")
}
if o.HomeLocation == nil && o.Server == nil {
return errors.New("one of home location or server is required")
}
return nil
}
// FloatingIPCreateResult is the result of creating a Floating IP.
type FloatingIPCreateResult struct {
FloatingIP *FloatingIP
Action *Action
}
// Create creates a Floating IP.
func (c *FloatingIPClient) Create(ctx context.Context, opts FloatingIPCreateOpts) (FloatingIPCreateResult, *Response, error) {
if err := opts.Validate(); err != nil {
return FloatingIPCreateResult{}, nil, err
}
reqBody := schema.FloatingIPCreateRequest{
Type: string(opts.Type),
Description: opts.Description,
Name: opts.Name,
}
if opts.HomeLocation != nil {
reqBody.HomeLocation = String(opts.HomeLocation.Name)
}
if opts.Server != nil {
reqBody.Server = Int(opts.Server.ID)
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return FloatingIPCreateResult{}, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/floating_ips", bytes.NewReader(reqBodyData))
if err != nil {
return FloatingIPCreateResult{}, nil, err
}
var respBody schema.FloatingIPCreateResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return FloatingIPCreateResult{}, resp, err
}
var action *Action
if respBody.Action != nil {
action = ActionFromSchema(*respBody.Action)
}
return FloatingIPCreateResult{
FloatingIP: FloatingIPFromSchema(respBody.FloatingIP),
Action: action,
}, resp, nil
}
// Delete deletes a Floating IP.
func (c *FloatingIPClient) Delete(ctx context.Context, floatingIP *FloatingIP) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/floating_ips/%d", floatingIP.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}
// FloatingIPUpdateOpts specifies options for updating a Floating IP.
type FloatingIPUpdateOpts struct {
Description string
Labels map[string]string
Name string
}
// Update updates a Floating IP.
func (c *FloatingIPClient) Update(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPUpdateOpts) (*FloatingIP, *Response, error) {
reqBody := schema.FloatingIPUpdateRequest{
Description: opts.Description,
Name: opts.Name,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/floating_ips/%d", floatingIP.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.FloatingIPUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return FloatingIPFromSchema(respBody.FloatingIP), resp, nil
}
// Assign assigns a Floating IP to a server.
func (c *FloatingIPClient) Assign(ctx context.Context, floatingIP *FloatingIP, server *Server) (*Action, *Response, error) {
reqBody := schema.FloatingIPActionAssignRequest{
Server: server.ID,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/floating_ips/%d/actions/assign", floatingIP.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.FloatingIPActionAssignResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// Unassign unassigns a Floating IP from the currently assigned server.
func (c *FloatingIPClient) Unassign(ctx context.Context, floatingIP *FloatingIP) (*Action, *Response, error) {
var reqBody schema.FloatingIPActionUnassignRequest
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/floating_ips/%d/actions/unassign", floatingIP.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.FloatingIPActionUnassignResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// ChangeDNSPtr changes or resets the reverse DNS pointer for a Floating IP address.
// Pass a nil ptr to reset the reverse DNS pointer to its default value.
func (c *FloatingIPClient) ChangeDNSPtr(ctx context.Context, floatingIP *FloatingIP, ip string, ptr *string) (*Action, *Response, error) {
reqBody := schema.FloatingIPActionChangeDNSPtrRequest{
IP: ip,
DNSPtr: ptr,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", floatingIP.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.FloatingIPActionChangeDNSPtrResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// FloatingIPChangeProtectionOpts specifies options for changing the resource protection level of a Floating IP.
type FloatingIPChangeProtectionOpts struct {
Delete *bool
}
// ChangeProtection changes the resource protection level of a Floating IP.
func (c *FloatingIPClient) ChangeProtection(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPChangeProtectionOpts) (*Action, *Response, error) {
reqBody := schema.FloatingIPActionChangeProtectionRequest{
Delete: opts.Delete,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/floating_ips/%d/actions/change_protection", floatingIP.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.FloatingIPActionChangeProtectionResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}

View file

@ -0,0 +1,5 @@
// Package hcloud is a library for the Hetzner Cloud API.
package hcloud
// Version is the library's version following Semantic Versioning.
const Version = "1.21.1"

View file

@ -0,0 +1,18 @@
package hcloud
import "time"
// String returns a pointer to the passed string s.
func String(s string) *string { return &s }
// Int returns a pointer to the passed integer i.
func Int(i int) *int { return &i }
// Bool returns a pointer to the passed bool b.
func Bool(b bool) *bool { return &b }
// Duration returns a pointer to the passed time.Duration d.
func Duration(d time.Duration) *time.Duration { return &d }
func intSlice(is []int) *[]int { return &is }
func stringSlice(ss []string) *[]string { return &ss }

View file

@ -0,0 +1,268 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Image represents an Image in the Hetzner Cloud.
type Image struct {
ID int
Name string
Type ImageType
Status ImageStatus
Description string
ImageSize float32
DiskSize float32
Created time.Time
CreatedFrom *Server
BoundTo *Server
RapidDeploy bool
OSFlavor string
OSVersion string
Protection ImageProtection
Deprecated time.Time // The zero value denotes the image is not deprecated.
Labels map[string]string
}
// IsDeprecated returns whether the image is deprecated.
func (image *Image) IsDeprecated() bool {
return !image.Deprecated.IsZero()
}
// ImageProtection represents the protection level of an image.
type ImageProtection struct {
Delete bool
}
// ImageType specifies the type of an image.
type ImageType string
const (
// ImageTypeSnapshot represents a snapshot image.
ImageTypeSnapshot ImageType = "snapshot"
// ImageTypeBackup represents a backup image.
ImageTypeBackup ImageType = "backup"
// ImageTypeSystem represents a system image.
ImageTypeSystem ImageType = "system"
)
// ImageStatus specifies the status of an image.
type ImageStatus string
const (
// ImageStatusCreating is the status when an image is being created.
ImageStatusCreating ImageStatus = "creating"
// ImageStatusAvailable is the status when an image is available.
ImageStatusAvailable ImageStatus = "available"
)
// ImageClient is a client for the image API.
type ImageClient struct {
client *Client
}
// GetByID retrieves an image by its ID. If the image does not exist, nil is returned.
func (c *ImageClient) GetByID(ctx context.Context, id int) (*Image, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/images/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.ImageGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return ImageFromSchema(body.Image), resp, nil
}
// GetByName retrieves an image by its name. If the image does not exist, nil is returned.
func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Response, error) {
if name == "" {
return nil, nil, nil
}
images, response, err := c.List(ctx, ImageListOpts{Name: name})
if len(images) == 0 {
return nil, response, err
}
return images[0], response, err
}
// Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it
// retrieves an image by its name. If the image does not exist, nil is returned.
func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// ImageListOpts specifies options for listing images.
type ImageListOpts struct {
ListOpts
Type []ImageType
BoundTo *Server
Name string
Sort []string
Status []ImageStatus
IncludeDeprecated bool
}
func (l ImageListOpts) values() url.Values {
vals := l.ListOpts.values()
for _, typ := range l.Type {
vals.Add("type", string(typ))
}
if l.BoundTo != nil {
vals.Add("bound_to", strconv.Itoa(l.BoundTo.ID))
}
if l.Name != "" {
vals.Add("name", l.Name)
}
if l.IncludeDeprecated {
vals.Add("include_deprecated", strconv.FormatBool(l.IncludeDeprecated))
}
for _, sort := range l.Sort {
vals.Add("sort", sort)
}
for _, status := range l.Status {
vals.Add("status", string(status))
}
return vals
}
// List returns a list of images for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *ImageClient) List(ctx context.Context, opts ImageListOpts) ([]*Image, *Response, error) {
path := "/images?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.ImageListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
images := make([]*Image, 0, len(body.Images))
for _, i := range body.Images {
images = append(images, ImageFromSchema(i))
}
return images, resp, nil
}
// All returns all images.
func (c *ImageClient) All(ctx context.Context) ([]*Image, error) {
return c.AllWithOpts(ctx, ImageListOpts{ListOpts: ListOpts{PerPage: 50}})
}
// AllWithOpts returns all images for the given options.
func (c *ImageClient) AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) {
allImages := []*Image{}
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
images, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allImages = append(allImages, images...)
return resp, nil
})
if err != nil {
return nil, err
}
return allImages, nil
}
// Delete deletes an image.
func (c *ImageClient) Delete(ctx context.Context, image *Image) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/images/%d", image.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}
// ImageUpdateOpts specifies options for updating an image.
type ImageUpdateOpts struct {
Description *string
Type ImageType
Labels map[string]string
}
// Update updates an image.
func (c *ImageClient) Update(ctx context.Context, image *Image, opts ImageUpdateOpts) (*Image, *Response, error) {
reqBody := schema.ImageUpdateRequest{
Description: opts.Description,
}
if opts.Type != "" {
reqBody.Type = String(string(opts.Type))
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/images/%d", image.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ImageUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ImageFromSchema(respBody.Image), resp, nil
}
// ImageChangeProtectionOpts specifies options for changing the resource protection level of an image.
type ImageChangeProtectionOpts struct {
Delete *bool
}
// ChangeProtection changes the resource protection level of an image.
func (c *ImageClient) ChangeProtection(ctx context.Context, image *Image, opts ImageChangeProtectionOpts) (*Action, *Response, error) {
reqBody := schema.ImageActionChangeProtectionRequest{
Delete: opts.Delete,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/images/%d/actions/change_protection", image.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ImageActionChangeProtectionResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}

139
vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go generated vendored Normal file
View file

@ -0,0 +1,139 @@
package hcloud
import (
"context"
"fmt"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// ISO represents an ISO image in the Hetzner Cloud.
type ISO struct {
ID int
Name string
Description string
Type ISOType
Deprecated time.Time
}
// IsDeprecated returns true if the ISO is deprecated
func (iso *ISO) IsDeprecated() bool {
return !iso.Deprecated.IsZero()
}
// ISOType specifies the type of an ISO image.
type ISOType string
const (
// ISOTypePublic is the type of a public ISO image.
ISOTypePublic ISOType = "public"
// ISOTypePrivate is the type of a private ISO image.
ISOTypePrivate ISOType = "private"
)
// ISOClient is a client for the ISO API.
type ISOClient struct {
client *Client
}
// GetByID retrieves an ISO by its ID.
func (c *ISOClient) GetByID(ctx context.Context, id int) (*ISO, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/isos/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.ISOGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, resp, err
}
return ISOFromSchema(body.ISO), resp, nil
}
// GetByName retrieves an ISO by its name.
func (c *ISOClient) GetByName(ctx context.Context, name string) (*ISO, *Response, error) {
if name == "" {
return nil, nil, nil
}
isos, response, err := c.List(ctx, ISOListOpts{Name: name})
if len(isos) == 0 {
return nil, response, err
}
return isos[0], response, err
}
// Get retrieves an ISO by its ID if the input can be parsed as an integer, otherwise it retrieves an ISO by its name.
func (c *ISOClient) Get(ctx context.Context, idOrName string) (*ISO, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// ISOListOpts specifies options for listing isos.
type ISOListOpts struct {
ListOpts
Name string
}
func (l ISOListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of ISOs for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *ISOClient) List(ctx context.Context, opts ISOListOpts) ([]*ISO, *Response, error) {
path := "/isos?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.ISOListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
isos := make([]*ISO, 0, len(body.ISOs))
for _, i := range body.ISOs {
isos = append(isos, ISOFromSchema(i))
}
return isos, resp, nil
}
// All returns all ISOs.
func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) {
allISOs := []*ISO{}
opts := ISOListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
isos, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allISOs = append(allISOs, isos...)
return resp, nil
})
if err != nil {
return nil, err
}
return allISOs, nil
}

View file

@ -0,0 +1,933 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// LoadBalancer represents a Load Balancer in the Hetzner Cloud.
type LoadBalancer struct {
ID int
Name string
PublicNet LoadBalancerPublicNet
PrivateNet []LoadBalancerPrivateNet
Location *Location
LoadBalancerType *LoadBalancerType
Algorithm LoadBalancerAlgorithm
Services []LoadBalancerService
Targets []LoadBalancerTarget
Protection LoadBalancerProtection
Labels map[string]string
Created time.Time
IncludedTraffic uint64
OutgoingTraffic uint64
IngoingTraffic uint64
}
// LoadBalancerPublicNet represents a Load Balancer's public network.
type LoadBalancerPublicNet struct {
Enabled bool
IPv4 LoadBalancerPublicNetIPv4
IPv6 LoadBalancerPublicNetIPv6
}
// LoadBalancerPublicNetIPv4 represents a Load Balancer's public IPv4 address.
type LoadBalancerPublicNetIPv4 struct {
IP net.IP
}
// LoadBalancerPublicNetIPv6 represents a Load Balancer's public IPv6 address.
type LoadBalancerPublicNetIPv6 struct {
IP net.IP
}
// LoadBalancerPrivateNet represents a Load Balancer's private network.
type LoadBalancerPrivateNet struct {
Network *Network
IP net.IP
}
// LoadBalancerService represents a Load Balancer service.
type LoadBalancerService struct {
Protocol LoadBalancerServiceProtocol
ListenPort int
DestinationPort int
Proxyprotocol bool
HTTP LoadBalancerServiceHTTP
HealthCheck LoadBalancerServiceHealthCheck
}
// LoadBalancerServiceHTTP stores configuration for a service using the HTTP protocol.
type LoadBalancerServiceHTTP struct {
CookieName string
CookieLifetime time.Duration
Certificates []*Certificate
RedirectHTTP bool
StickySessions bool
}
// LoadBalancerServiceHealthCheck stores configuration for a service health check.
type LoadBalancerServiceHealthCheck struct {
Protocol LoadBalancerServiceProtocol
Port int
Interval time.Duration
Timeout time.Duration
Retries int
HTTP *LoadBalancerServiceHealthCheckHTTP
}
// LoadBalancerServiceHealthCheckHTTP stores configuration for a service health check
// using the HTTP protocol.
type LoadBalancerServiceHealthCheckHTTP struct {
Domain string
Path string
Response string
StatusCodes []string
TLS bool
}
// LoadBalancerAlgorithmType specifies the algorithm type a Load Balancer
// uses for distributing requests.
type LoadBalancerAlgorithmType string
const (
// LoadBalancerAlgorithmTypeRoundRobin is an algorithm which distributes
// requests to targets in a round robin fashion.
LoadBalancerAlgorithmTypeRoundRobin LoadBalancerAlgorithmType = "round_robin"
// LoadBalancerAlgorithmTypeLeastConnections is an algorithm which distributes
// requests to targets with the least number of connections.
LoadBalancerAlgorithmTypeLeastConnections LoadBalancerAlgorithmType = "least_connections"
)
// LoadBalancerAlgorithm configures the algorithm a Load Balancer uses
// for distributing requests.
type LoadBalancerAlgorithm struct {
Type LoadBalancerAlgorithmType
}
// LoadBalancerTargetType specifies the type of a Load Balancer target.
type LoadBalancerTargetType string
const (
// LoadBalancerTargetTypeServer is a target type which points to a specific
// server.
LoadBalancerTargetTypeServer LoadBalancerTargetType = "server"
// LoadBalancerTargetTypeLabelSelector is a target type which selects the
// servers a Load Balancer points to using labels assigned to the servers.
LoadBalancerTargetTypeLabelSelector LoadBalancerTargetType = "label_selector"
// LoadBalancerTargetTypeIP is a target type which points to an IP.
LoadBalancerTargetTypeIP LoadBalancerTargetType = "ip"
)
// LoadBalancerServiceProtocol specifies the protocol of a Load Balancer service.
type LoadBalancerServiceProtocol string
const (
// LoadBalancerServiceProtocolTCP specifies a TCP service.
LoadBalancerServiceProtocolTCP LoadBalancerServiceProtocol = "tcp"
// LoadBalancerServiceProtocolHTTP specifies an HTTP service.
LoadBalancerServiceProtocolHTTP LoadBalancerServiceProtocol = "http"
// LoadBalancerServiceProtocolHTTPS specifies an HTTPS service.
LoadBalancerServiceProtocolHTTPS LoadBalancerServiceProtocol = "https"
)
// LoadBalancerTarget represents a Load Balancer target.
type LoadBalancerTarget struct {
Type LoadBalancerTargetType
Server *LoadBalancerTargetServer
LabelSelector *LoadBalancerTargetLabelSelector
IP *LoadBalancerTargetIP
HealthStatus []LoadBalancerTargetHealthStatus
Targets []LoadBalancerTarget
UsePrivateIP bool
}
// LoadBalancerTargetServer configures a Load Balancer target
// pointing at a specific server.
type LoadBalancerTargetServer struct {
Server *Server
}
// LoadBalancerTargetLabelSelector configures a Load Balancer target pointing
// at the servers matching the selector. This includes the target pointing at
// nothing, if no servers match the Selector.
type LoadBalancerTargetLabelSelector struct {
Selector string
}
// LoadBalancerTargetIP configures a Load Balancer target pointing to a Hetzner
// Online IP address.
type LoadBalancerTargetIP struct {
IP string
}
// LoadBalancerTargetHealthStatusStatus describes a target's health status.
type LoadBalancerTargetHealthStatusStatus string
const (
// LoadBalancerTargetHealthStatusStatusUnknown denotes that the health status is unknown.
LoadBalancerTargetHealthStatusStatusUnknown LoadBalancerTargetHealthStatusStatus = "unknown"
// LoadBalancerTargetHealthStatusStatusHealthy denotes a healthy target.
LoadBalancerTargetHealthStatusStatusHealthy LoadBalancerTargetHealthStatusStatus = "healthy"
// LoadBalancerTargetHealthStatusStatusUnhealthy denotes an unhealthy target.
LoadBalancerTargetHealthStatusStatusUnhealthy LoadBalancerTargetHealthStatusStatus = "unhealthy"
)
// LoadBalancerTargetHealthStatus describes a target's health for a specific service.
type LoadBalancerTargetHealthStatus struct {
ListenPort int
Status LoadBalancerTargetHealthStatusStatus
}
// LoadBalancerProtection represents the protection level of a Load Balancer.
type LoadBalancerProtection struct {
Delete bool
}
// LoadBalancerClient is a client for the Load Balancers API.
type LoadBalancerClient struct {
client *Client
}
// GetByID retrieves a Load Balancer by its ID. If the Load Balancer does not exist, nil is returned.
func (c *LoadBalancerClient) GetByID(ctx context.Context, id int) (*LoadBalancer, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/load_balancers/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.LoadBalancerGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return LoadBalancerFromSchema(body.LoadBalancer), resp, nil
}
// GetByName retrieves a Load Balancer by its name. If the Load Balancer does not exist, nil is returned.
func (c *LoadBalancerClient) GetByName(ctx context.Context, name string) (*LoadBalancer, *Response, error) {
if name == "" {
return nil, nil, nil
}
LoadBalancer, response, err := c.List(ctx, LoadBalancerListOpts{Name: name})
if len(LoadBalancer) == 0 {
return nil, response, err
}
return LoadBalancer[0], response, err
}
// Get retrieves a Load Balancer by its ID if the input can be parsed as an integer, otherwise it
// retrieves a Load Balancer by its name. If the Load Balancer does not exist, nil is returned.
func (c *LoadBalancerClient) Get(ctx context.Context, idOrName string) (*LoadBalancer, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// LoadBalancerListOpts specifies options for listing Load Balancers.
type LoadBalancerListOpts struct {
ListOpts
Name string
}
func (l LoadBalancerListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of Load Balancers for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *LoadBalancerClient) List(ctx context.Context, opts LoadBalancerListOpts) ([]*LoadBalancer, *Response, error) {
path := "/load_balancers?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.LoadBalancerListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
LoadBalancers := make([]*LoadBalancer, 0, len(body.LoadBalancers))
for _, s := range body.LoadBalancers {
LoadBalancers = append(LoadBalancers, LoadBalancerFromSchema(s))
}
return LoadBalancers, resp, nil
}
// All returns all Load Balancers.
func (c *LoadBalancerClient) All(ctx context.Context) ([]*LoadBalancer, error) {
allLoadBalancer := []*LoadBalancer{}
opts := LoadBalancerListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
LoadBalancer, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allLoadBalancer = append(allLoadBalancer, LoadBalancer...)
return resp, nil
})
if err != nil {
return nil, err
}
return allLoadBalancer, nil
}
// AllWithOpts returns all Load Balancers for the given options.
func (c *LoadBalancerClient) AllWithOpts(ctx context.Context, opts LoadBalancerListOpts) ([]*LoadBalancer, error) {
var allLoadBalancers []*LoadBalancer
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
LoadBalancers, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allLoadBalancers = append(allLoadBalancers, LoadBalancers...)
return resp, nil
})
if err != nil {
return nil, err
}
return allLoadBalancers, nil
}
// LoadBalancerUpdateOpts specifies options for updating a Load Balancer.
type LoadBalancerUpdateOpts struct {
Name string
Labels map[string]string
}
// Update updates a Load Balancer.
func (c *LoadBalancerClient) Update(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerUpdateOpts) (*LoadBalancer, *Response, error) {
reqBody := schema.LoadBalancerUpdateRequest{}
if opts.Name != "" {
reqBody.Name = &opts.Name
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return LoadBalancerFromSchema(respBody.LoadBalancer), resp, nil
}
// LoadBalancerCreateOpts specifies options for creating a new Load Balancer.
type LoadBalancerCreateOpts struct {
Name string
LoadBalancerType *LoadBalancerType
Algorithm *LoadBalancerAlgorithm
Location *Location
NetworkZone NetworkZone
Labels map[string]string
Targets []LoadBalancerCreateOptsTarget
Services []LoadBalancerCreateOptsService
PublicInterface *bool
Network *Network
}
// LoadBalancerCreateOptsTarget holds options for specifying a target
// when creating a new Load Balancer.
type LoadBalancerCreateOptsTarget struct {
Type LoadBalancerTargetType
Server LoadBalancerCreateOptsTargetServer
LabelSelector LoadBalancerCreateOptsTargetLabelSelector
IP LoadBalancerCreateOptsTargetIP
UsePrivateIP *bool
}
// LoadBalancerCreateOptsTargetServer holds options for specifying a server target
// when creating a new Load Balancer.
type LoadBalancerCreateOptsTargetServer struct {
Server *Server
}
// LoadBalancerCreateOptsTargetLabelSelector holds options for specifying a label selector target
// when creating a new Load Balancer.
type LoadBalancerCreateOptsTargetLabelSelector struct {
Selector string
}
// LoadBalancerCreateOptsTargetIP holds options for specifying an IP target
// when creating a new Load Balancer.
type LoadBalancerCreateOptsTargetIP struct {
IP string
}
// LoadBalancerCreateOptsService holds options for specifying a service
// when creating a new Load Balancer.
type LoadBalancerCreateOptsService struct {
Protocol LoadBalancerServiceProtocol
ListenPort *int
DestinationPort *int
Proxyprotocol *bool
HTTP *LoadBalancerCreateOptsServiceHTTP
HealthCheck *LoadBalancerCreateOptsServiceHealthCheck
}
// LoadBalancerCreateOptsServiceHTTP holds options for specifying an HTTP service
// when creating a new Load Balancer.
type LoadBalancerCreateOptsServiceHTTP struct {
CookieName *string
CookieLifetime *time.Duration
Certificates []*Certificate
RedirectHTTP *bool
StickySessions *bool
}
// LoadBalancerCreateOptsServiceHealthCheck holds options for specifying a service
// health check when creating a new Load Balancer.
type LoadBalancerCreateOptsServiceHealthCheck struct {
Protocol LoadBalancerServiceProtocol
Port *int
Interval *time.Duration
Timeout *time.Duration
Retries *int
HTTP *LoadBalancerCreateOptsServiceHealthCheckHTTP
}
// LoadBalancerCreateOptsServiceHealthCheckHTTP holds options for specifying a service
// HTTP health check when creating a new Load Balancer.
type LoadBalancerCreateOptsServiceHealthCheckHTTP struct {
Domain *string
Path *string
Response *string
StatusCodes []string
TLS *bool
}
// LoadBalancerCreateResult is the result of a create Load Balancer call.
type LoadBalancerCreateResult struct {
LoadBalancer *LoadBalancer
Action *Action
}
// Create creates a new Load Balancer.
func (c *LoadBalancerClient) Create(ctx context.Context, opts LoadBalancerCreateOpts) (LoadBalancerCreateResult, *Response, error) {
reqBody := loadBalancerCreateOptsToSchema(opts)
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return LoadBalancerCreateResult{}, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/load_balancers", bytes.NewReader(reqBodyData))
if err != nil {
return LoadBalancerCreateResult{}, nil, err
}
respBody := schema.LoadBalancerCreateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return LoadBalancerCreateResult{}, resp, err
}
return LoadBalancerCreateResult{
LoadBalancer: LoadBalancerFromSchema(respBody.LoadBalancer),
Action: ActionFromSchema(respBody.Action),
}, resp, nil
}
// Delete deletes a Load Balancer.
func (c *LoadBalancerClient) Delete(ctx context.Context, loadBalancer *LoadBalancer) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/load_balancers/%d", loadBalancer.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}
func (c *LoadBalancerClient) addTarget(ctx context.Context, loadBalancer *LoadBalancer, reqBody schema.LoadBalancerActionAddTargetRequest) (*Action, *Response, error) {
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/add_target", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.LoadBalancerActionAddTargetResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
func (c *LoadBalancerClient) removeTarget(ctx context.Context, loadBalancer *LoadBalancer, reqBody schema.LoadBalancerActionRemoveTargetRequest) (*Action, *Response, error) {
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/remove_target", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.LoadBalancerActionRemoveTargetResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// LoadBalancerAddServerTargetOpts specifies options for adding a server target
// to a Load Balancer.
type LoadBalancerAddServerTargetOpts struct {
Server *Server
UsePrivateIP *bool
}
// AddServerTarget adds a server target to a Load Balancer.
func (c *LoadBalancerClient) AddServerTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddServerTargetOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionAddTargetRequest{
Type: string(LoadBalancerTargetTypeServer),
Server: &schema.LoadBalancerActionAddTargetRequestServer{
ID: opts.Server.ID,
},
UsePrivateIP: opts.UsePrivateIP,
}
return c.addTarget(ctx, loadBalancer, reqBody)
}
// RemoveServerTarget removes a server target from a Load Balancer.
func (c *LoadBalancerClient) RemoveServerTarget(ctx context.Context, loadBalancer *LoadBalancer, server *Server) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionRemoveTargetRequest{
Type: string(LoadBalancerTargetTypeServer),
Server: &schema.LoadBalancerActionRemoveTargetRequestServer{
ID: server.ID,
},
}
return c.removeTarget(ctx, loadBalancer, reqBody)
}
// LoadBalancerAddLabelSelectorTargetOpts specifies options for adding a label selector target
// to a Load Balancer.
type LoadBalancerAddLabelSelectorTargetOpts struct {
Selector string
UsePrivateIP *bool
}
// AddLabelSelectorTarget adds a label selector target to a Load Balancer.
func (c *LoadBalancerClient) AddLabelSelectorTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddLabelSelectorTargetOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionAddTargetRequest{
Type: string(LoadBalancerTargetTypeLabelSelector),
LabelSelector: &schema.LoadBalancerActionAddTargetRequestLabelSelector{
Selector: opts.Selector,
},
UsePrivateIP: opts.UsePrivateIP,
}
return c.addTarget(ctx, loadBalancer, reqBody)
}
// RemoveLabelSelectorTarget removes a label selector target from a Load Balancer.
func (c *LoadBalancerClient) RemoveLabelSelectorTarget(ctx context.Context, loadBalancer *LoadBalancer, labelSelector string) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionRemoveTargetRequest{
Type: string(LoadBalancerTargetTypeLabelSelector),
LabelSelector: &schema.LoadBalancerActionRemoveTargetRequestLabelSelector{
Selector: labelSelector,
},
}
return c.removeTarget(ctx, loadBalancer, reqBody)
}
// LoadBalancerAddIPTargetOpts specifies options for adding an IP target to a
// Load Balancer.
type LoadBalancerAddIPTargetOpts struct {
IP net.IP
}
// AddIPTarget adds an IP target to a Load Balancer.
func (c *LoadBalancerClient) AddIPTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddIPTargetOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionAddTargetRequest{
Type: string(LoadBalancerTargetTypeIP),
IP: &schema.LoadBalancerActionAddTargetRequestIP{IP: opts.IP.String()},
}
return c.addTarget(ctx, loadBalancer, reqBody)
}
// RemoveIPTarget removes an IP target from a Load Balancer.
func (c *LoadBalancerClient) RemoveIPTarget(ctx context.Context, loadBalancer *LoadBalancer, ip net.IP) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionRemoveTargetRequest{
Type: string(LoadBalancerTargetTypeIP),
IP: &schema.LoadBalancerActionRemoveTargetRequestIP{
IP: ip.String(),
},
}
return c.removeTarget(ctx, loadBalancer, reqBody)
}
// LoadBalancerAddServiceOpts specifies options for adding a service to a Load Balancer.
type LoadBalancerAddServiceOpts struct {
Protocol LoadBalancerServiceProtocol
ListenPort *int
DestinationPort *int
Proxyprotocol *bool
HTTP *LoadBalancerAddServiceOptsHTTP
HealthCheck *LoadBalancerAddServiceOptsHealthCheck
}
// LoadBalancerAddServiceOptsHTTP holds options for specifying an HTTP service
// when adding a service to a Load Balancer.
type LoadBalancerAddServiceOptsHTTP struct {
CookieName *string
CookieLifetime *time.Duration
Certificates []*Certificate
RedirectHTTP *bool
StickySessions *bool
}
// LoadBalancerAddServiceOptsHealthCheck holds options for specifying an health check
// when adding a service to a Load Balancer.
type LoadBalancerAddServiceOptsHealthCheck struct {
Protocol LoadBalancerServiceProtocol
Port *int
Interval *time.Duration
Timeout *time.Duration
Retries *int
HTTP *LoadBalancerAddServiceOptsHealthCheckHTTP
}
// LoadBalancerAddServiceOptsHealthCheckHTTP holds options for specifying an
// HTTP health check when adding a service to a Load Balancer.
type LoadBalancerAddServiceOptsHealthCheckHTTP struct {
Domain *string
Path *string
Response *string
StatusCodes []string
TLS *bool
}
// AddService adds a service to a Load Balancer.
func (c *LoadBalancerClient) AddService(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddServiceOpts) (*Action, *Response, error) {
reqBody := loadBalancerAddServiceOptsToSchema(opts)
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/add_service", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.LoadBalancerActionAddServiceResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// LoadBalancerUpdateServiceOpts specifies options for updating a service.
type LoadBalancerUpdateServiceOpts struct {
Protocol LoadBalancerServiceProtocol
DestinationPort *int
Proxyprotocol *bool
HTTP *LoadBalancerUpdateServiceOptsHTTP
HealthCheck *LoadBalancerUpdateServiceOptsHealthCheck
}
// LoadBalancerUpdateServiceOptsHTTP specifies options for updating an HTTP(S) service.
type LoadBalancerUpdateServiceOptsHTTP struct {
CookieName *string
CookieLifetime *time.Duration
Certificates []*Certificate
RedirectHTTP *bool
StickySessions *bool
}
// LoadBalancerUpdateServiceOptsHealthCheck specifies options for updating
// a service's health check.
type LoadBalancerUpdateServiceOptsHealthCheck struct {
Protocol LoadBalancerServiceProtocol
Port *int
Interval *time.Duration
Timeout *time.Duration
Retries *int
HTTP *LoadBalancerUpdateServiceOptsHealthCheckHTTP
}
// LoadBalancerUpdateServiceOptsHealthCheckHTTP specifies options for updating
// the HTTP-specific settings of a service's health check.
type LoadBalancerUpdateServiceOptsHealthCheckHTTP struct {
Domain *string
Path *string
Response *string
StatusCodes []string
TLS *bool
}
// UpdateService updates a Load Balancer service.
func (c *LoadBalancerClient) UpdateService(ctx context.Context, loadBalancer *LoadBalancer, listenPort int, opts LoadBalancerUpdateServiceOpts) (*Action, *Response, error) {
reqBody := loadBalancerUpdateServiceOptsToSchema(opts)
reqBody.ListenPort = listenPort
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/update_service", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.LoadBalancerActionUpdateServiceResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// DeleteService deletes a Load Balancer service.
func (c *LoadBalancerClient) DeleteService(ctx context.Context, loadBalancer *LoadBalancer, listenPort int) (*Action, *Response, error) {
reqBody := schema.LoadBalancerDeleteServiceRequest{
ListenPort: listenPort,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/delete_service", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.LoadBalancerDeleteServiceResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// LoadBalancerChangeProtectionOpts specifies options for changing the resource protection level of a Load Balancer.
type LoadBalancerChangeProtectionOpts struct {
Delete *bool
}
// ChangeProtection changes the resource protection level of a Load Balancer.
func (c *LoadBalancerClient) ChangeProtection(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeProtectionOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionChangeProtectionRequest{
Delete: opts.Delete,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/change_protection", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerActionChangeProtectionResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// LoadBalancerChangeAlgorithmOpts specifies options for changing the algorithm of a Load Balancer.
type LoadBalancerChangeAlgorithmOpts struct {
Type LoadBalancerAlgorithmType
}
// ChangeAlgorithm changes the algorithm of a Load Balancer.
func (c *LoadBalancerClient) ChangeAlgorithm(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeAlgorithmOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionChangeAlgorithmRequest{
Type: string(opts.Type),
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/change_algorithm", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerActionChangeAlgorithmResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// LoadBalancerAttachToNetworkOpts specifies options for attaching a Load Balancer to a network.
type LoadBalancerAttachToNetworkOpts struct {
Network *Network
IP net.IP
}
// AttachToNetwork attaches a Load Balancer to a network.
func (c *LoadBalancerClient) AttachToNetwork(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAttachToNetworkOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionAttachToNetworkRequest{
Network: opts.Network.ID,
}
if opts.IP != nil {
reqBody.IP = String(opts.IP.String())
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/attach_to_network", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerActionAttachToNetworkResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// LoadBalancerDetachFromNetworkOpts specifies options for detaching a Load Balancer from a network.
type LoadBalancerDetachFromNetworkOpts struct {
Network *Network
}
// DetachFromNetwork detaches a Load Balancer from a network.
func (c *LoadBalancerClient) DetachFromNetwork(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerDetachFromNetworkOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionDetachFromNetworkRequest{
Network: opts.Network.ID,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/detach_from_network", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerActionDetachFromNetworkResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// EnablePublicInterface enables the Load Balancer's public network interface.
func (c *LoadBalancerClient) EnablePublicInterface(ctx context.Context, loadBalancer *LoadBalancer) (*Action, *Response, error) {
path := fmt.Sprintf("/load_balancers/%d/actions/enable_public_interface", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerActionEnablePublicInterfaceResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// DisablePublicInterface disables the Load Balancer's public network interface.
func (c *LoadBalancerClient) DisablePublicInterface(ctx context.Context, loadBalancer *LoadBalancer) (*Action, *Response, error) {
path := fmt.Sprintf("/load_balancers/%d/actions/disable_public_interface", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerActionDisablePublicInterfaceResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// LoadBalancerChangeTypeOpts specifies options for changing a Load Balancer's type.
type LoadBalancerChangeTypeOpts struct {
LoadBalancerType *LoadBalancerType // new Load Balancer type
}
// ChangeType changes a Load Balancer's type.
func (c *LoadBalancerClient) ChangeType(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeTypeOpts) (*Action, *Response, error) {
reqBody := schema.LoadBalancerActionChangeTypeRequest{}
if opts.LoadBalancerType.ID != 0 {
reqBody.LoadBalancerType = opts.LoadBalancerType.ID
} else {
reqBody.LoadBalancerType = opts.LoadBalancerType.Name
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/load_balancers/%d/actions/change_type", loadBalancer.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.LoadBalancerActionChangeTypeResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}

View file

@ -0,0 +1,126 @@
package hcloud
import (
"context"
"fmt"
"net/url"
"strconv"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// LoadBalancerType represents a LoadBalancer type in the Hetzner Cloud.
type LoadBalancerType struct {
ID int
Name string
Description string
MaxConnections int
MaxServices int
MaxTargets int
MaxAssignedCertificates int
Pricings []LoadBalancerTypeLocationPricing
}
// LoadBalancerTypeClient is a client for the Load Balancer types API.
type LoadBalancerTypeClient struct {
client *Client
}
// GetByID retrieves a Load Balancer type by its ID. If the Load Balancer type does not exist, nil is returned.
func (c *LoadBalancerTypeClient) GetByID(ctx context.Context, id int) (*LoadBalancerType, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/load_balancer_types/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.LoadBalancerTypeGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return LoadBalancerTypeFromSchema(body.LoadBalancerType), resp, nil
}
// GetByName retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned.
func (c *LoadBalancerTypeClient) GetByName(ctx context.Context, name string) (*LoadBalancerType, *Response, error) {
if name == "" {
return nil, nil, nil
}
LoadBalancerTypes, response, err := c.List(ctx, LoadBalancerTypeListOpts{Name: name})
if len(LoadBalancerTypes) == 0 {
return nil, response, err
}
return LoadBalancerTypes[0], response, err
}
// Get retrieves a Load Balancer type by its ID if the input can be parsed as an integer, otherwise it
// retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned.
func (c *LoadBalancerTypeClient) Get(ctx context.Context, idOrName string) (*LoadBalancerType, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// LoadBalancerTypeListOpts specifies options for listing Load Balancer types.
type LoadBalancerTypeListOpts struct {
ListOpts
Name string
}
func (l LoadBalancerTypeListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of Load Balancer types for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *LoadBalancerTypeClient) List(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, *Response, error) {
path := "/load_balancer_types?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.LoadBalancerTypeListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
LoadBalancerTypes := make([]*LoadBalancerType, 0, len(body.LoadBalancerTypes))
for _, s := range body.LoadBalancerTypes {
LoadBalancerTypes = append(LoadBalancerTypes, LoadBalancerTypeFromSchema(s))
}
return LoadBalancerTypes, resp, nil
}
// All returns all Load Balancer types.
func (c *LoadBalancerTypeClient) All(ctx context.Context) ([]*LoadBalancerType, error) {
allLoadBalancerTypes := []*LoadBalancerType{}
opts := LoadBalancerTypeListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
LoadBalancerTypes, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allLoadBalancerTypes = append(allLoadBalancerTypes, LoadBalancerTypes...)
return resp, nil
})
if err != nil {
return nil, err
}
return allLoadBalancerTypes, nil
}

View file

@ -0,0 +1,126 @@
package hcloud
import (
"context"
"fmt"
"net/url"
"strconv"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Location represents a location in the Hetzner Cloud.
type Location struct {
ID int
Name string
Description string
Country string
City string
Latitude float64
Longitude float64
NetworkZone NetworkZone
}
// LocationClient is a client for the location API.
type LocationClient struct {
client *Client
}
// GetByID retrieves a location by its ID. If the location does not exist, nil is returned.
func (c *LocationClient) GetByID(ctx context.Context, id int) (*Location, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/locations/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.LocationGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, resp, err
}
return LocationFromSchema(body.Location), resp, nil
}
// GetByName retrieves an location by its name. If the location does not exist, nil is returned.
func (c *LocationClient) GetByName(ctx context.Context, name string) (*Location, *Response, error) {
if name == "" {
return nil, nil, nil
}
locations, response, err := c.List(ctx, LocationListOpts{Name: name})
if len(locations) == 0 {
return nil, response, err
}
return locations[0], response, err
}
// Get retrieves a location by its ID if the input can be parsed as an integer, otherwise it
// retrieves a location by its name. If the location does not exist, nil is returned.
func (c *LocationClient) Get(ctx context.Context, idOrName string) (*Location, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// LocationListOpts specifies options for listing location.
type LocationListOpts struct {
ListOpts
Name string
}
func (l LocationListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of locations for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *LocationClient) List(ctx context.Context, opts LocationListOpts) ([]*Location, *Response, error) {
path := "/locations?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.LocationListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
locations := make([]*Location, 0, len(body.Locations))
for _, i := range body.Locations {
locations = append(locations, LocationFromSchema(i))
}
return locations, resp, nil
}
// All returns all locations.
func (c *LocationClient) All(ctx context.Context) ([]*Location, error) {
allLocations := []*Location{}
opts := LocationListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
locations, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allLocations = append(allLocations, locations...)
return resp, nil
})
if err != nil {
return nil, err
}
return allLocations, nil
}

View file

@ -0,0 +1,454 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// NetworkZone specifies a network zone.
type NetworkZone string
// List of available Network Zones.
const (
NetworkZoneEUCentral NetworkZone = "eu-central"
)
// NetworkSubnetType specifies a type of a subnet.
type NetworkSubnetType string
// List of available network subnet types.
const (
NetworkSubnetTypeCloud NetworkSubnetType = "cloud"
NetworkSubnetTypeServer NetworkSubnetType = "server"
)
// Network represents a network in the Hetzner Cloud.
type Network struct {
ID int
Name string
Created time.Time
IPRange *net.IPNet
Subnets []NetworkSubnet
Routes []NetworkRoute
Servers []*Server
Protection NetworkProtection
Labels map[string]string
}
// NetworkSubnet represents a subnet of a network in the Hetzner Cloud.
type NetworkSubnet struct {
Type NetworkSubnetType
IPRange *net.IPNet
NetworkZone NetworkZone
Gateway net.IP
}
// NetworkRoute represents a route of a network.
type NetworkRoute struct {
Destination *net.IPNet
Gateway net.IP
}
// NetworkProtection represents the protection level of a network.
type NetworkProtection struct {
Delete bool
}
// NetworkClient is a client for the network API.
type NetworkClient struct {
client *Client
}
// GetByID retrieves a network by its ID. If the network does not exist, nil is returned.
func (c *NetworkClient) GetByID(ctx context.Context, id int) (*Network, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/networks/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.NetworkGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return NetworkFromSchema(body.Network), resp, nil
}
// GetByName retrieves a network by its name. If the network does not exist, nil is returned.
func (c *NetworkClient) GetByName(ctx context.Context, name string) (*Network, *Response, error) {
if name == "" {
return nil, nil, nil
}
Networks, response, err := c.List(ctx, NetworkListOpts{Name: name})
if len(Networks) == 0 {
return nil, response, err
}
return Networks[0], response, err
}
// Get retrieves a network by its ID if the input can be parsed as an integer, otherwise it
// retrieves a network by its name. If the network does not exist, nil is returned.
func (c *NetworkClient) Get(ctx context.Context, idOrName string) (*Network, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// NetworkListOpts specifies options for listing networks.
type NetworkListOpts struct {
ListOpts
Name string
}
func (l NetworkListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of networks for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *NetworkClient) List(ctx context.Context, opts NetworkListOpts) ([]*Network, *Response, error) {
path := "/networks?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.NetworkListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
Networks := make([]*Network, 0, len(body.Networks))
for _, s := range body.Networks {
Networks = append(Networks, NetworkFromSchema(s))
}
return Networks, resp, nil
}
// All returns all networks.
func (c *NetworkClient) All(ctx context.Context) ([]*Network, error) {
return c.AllWithOpts(ctx, NetworkListOpts{ListOpts: ListOpts{PerPage: 50}})
}
// AllWithOpts returns all networks for the given options.
func (c *NetworkClient) AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) {
var allNetworks []*Network
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
Networks, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allNetworks = append(allNetworks, Networks...)
return resp, nil
})
if err != nil {
return nil, err
}
return allNetworks, nil
}
// Delete deletes a network.
func (c *NetworkClient) Delete(ctx context.Context, network *Network) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/networks/%d", network.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}
// NetworkUpdateOpts specifies options for updating a network.
type NetworkUpdateOpts struct {
Name string
Labels map[string]string
}
// Update updates a network.
func (c *NetworkClient) Update(ctx context.Context, network *Network, opts NetworkUpdateOpts) (*Network, *Response, error) {
reqBody := schema.NetworkUpdateRequest{
Name: opts.Name,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/networks/%d", network.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return NetworkFromSchema(respBody.Network), resp, nil
}
// NetworkCreateOpts specifies options for creating a new network.
type NetworkCreateOpts struct {
Name string
IPRange *net.IPNet
Subnets []NetworkSubnet
Routes []NetworkRoute
Labels map[string]string
}
// Validate checks if options are valid.
func (o NetworkCreateOpts) Validate() error {
if o.Name == "" {
return errors.New("missing name")
}
if o.IPRange == nil || o.IPRange.String() == "" {
return errors.New("missing IP range")
}
return nil
}
// Create creates a new network.
func (c *NetworkClient) Create(ctx context.Context, opts NetworkCreateOpts) (*Network, *Response, error) {
if err := opts.Validate(); err != nil {
return nil, nil, err
}
reqBody := schema.NetworkCreateRequest{
Name: opts.Name,
IPRange: opts.IPRange.String(),
}
for _, subnet := range opts.Subnets {
reqBody.Subnets = append(reqBody.Subnets, schema.NetworkSubnet{
Type: string(subnet.Type),
IPRange: subnet.IPRange.String(),
NetworkZone: string(subnet.NetworkZone),
})
}
for _, route := range opts.Routes {
reqBody.Routes = append(reqBody.Routes, schema.NetworkRoute{
Destination: route.Destination.String(),
Gateway: route.Gateway.String(),
})
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/networks", bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkCreateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return NetworkFromSchema(respBody.Network), resp, nil
}
// NetworkChangeIPRangeOpts specifies options for changing the IP range of a network.
type NetworkChangeIPRangeOpts struct {
IPRange *net.IPNet
}
// ChangeIPRange changes the IP range of a network.
func (c *NetworkClient) ChangeIPRange(ctx context.Context, network *Network, opts NetworkChangeIPRangeOpts) (*Action, *Response, error) {
reqBody := schema.NetworkActionChangeIPRangeRequest{
IPRange: opts.IPRange.String(),
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/networks/%d/actions/change_ip_range", network.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkActionChangeIPRangeResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// NetworkAddSubnetOpts specifies options for adding a subnet to a network.
type NetworkAddSubnetOpts struct {
Subnet NetworkSubnet
}
// AddSubnet adds a subnet to a network.
func (c *NetworkClient) AddSubnet(ctx context.Context, network *Network, opts NetworkAddSubnetOpts) (*Action, *Response, error) {
reqBody := schema.NetworkActionAddSubnetRequest{
Type: string(opts.Subnet.Type),
NetworkZone: string(opts.Subnet.NetworkZone),
}
if opts.Subnet.IPRange != nil {
reqBody.IPRange = opts.Subnet.IPRange.String()
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/networks/%d/actions/add_subnet", network.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkActionAddSubnetResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// NetworkDeleteSubnetOpts specifies options for deleting a subnet from a network.
type NetworkDeleteSubnetOpts struct {
Subnet NetworkSubnet
}
// DeleteSubnet deletes a subnet from a network.
func (c *NetworkClient) DeleteSubnet(ctx context.Context, network *Network, opts NetworkDeleteSubnetOpts) (*Action, *Response, error) {
reqBody := schema.NetworkActionDeleteSubnetRequest{
IPRange: opts.Subnet.IPRange.String(),
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/networks/%d/actions/delete_subnet", network.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkActionDeleteSubnetResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// NetworkAddRouteOpts specifies options for adding a route to a network.
type NetworkAddRouteOpts struct {
Route NetworkRoute
}
// AddRoute adds a route to a network.
func (c *NetworkClient) AddRoute(ctx context.Context, network *Network, opts NetworkAddRouteOpts) (*Action, *Response, error) {
reqBody := schema.NetworkActionAddRouteRequest{
Destination: opts.Route.Destination.String(),
Gateway: opts.Route.Gateway.String(),
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/networks/%d/actions/add_route", network.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkActionAddSubnetResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// NetworkDeleteRouteOpts specifies options for deleting a route from a network.
type NetworkDeleteRouteOpts struct {
Route NetworkRoute
}
// DeleteRoute deletes a route from a network.
func (c *NetworkClient) DeleteRoute(ctx context.Context, network *Network, opts NetworkDeleteRouteOpts) (*Action, *Response, error) {
reqBody := schema.NetworkActionDeleteRouteRequest{
Destination: opts.Route.Destination.String(),
Gateway: opts.Route.Gateway.String(),
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/networks/%d/actions/delete_route", network.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkActionDeleteSubnetResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// NetworkChangeProtectionOpts specifies options for changing the resource protection level of a network.
type NetworkChangeProtectionOpts struct {
Delete *bool
}
// ChangeProtection changes the resource protection level of a network.
func (c *NetworkClient) ChangeProtection(ctx context.Context, network *Network, opts NetworkChangeProtectionOpts) (*Action, *Response, error) {
reqBody := schema.NetworkActionChangeProtectionRequest{
Delete: opts.Delete,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/networks/%d/actions/change_protection", network.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.NetworkActionChangeProtectionResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}

View file

@ -0,0 +1,95 @@
package hcloud
import (
"context"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Pricing specifies pricing information for various resources.
type Pricing struct {
Image ImagePricing
FloatingIP FloatingIPPricing
Traffic TrafficPricing
ServerBackup ServerBackupPricing
ServerTypes []ServerTypePricing
LoadBalancerTypes []LoadBalancerTypePricing
}
// Price represents a price. Net amount, gross amount, as well as VAT rate are
// specified as strings and it is the user's responsibility to convert them to
// appropriate types for calculations.
type Price struct {
Currency string
VATRate string
Net string
Gross string
}
// ImagePricing provides pricing information for imaegs.
type ImagePricing struct {
PerGBMonth Price
}
// FloatingIPPricing provides pricing information for Floating IPs.
type FloatingIPPricing struct {
Monthly Price
}
// TrafficPricing provides pricing information for traffic.
type TrafficPricing struct {
PerTB Price
}
// ServerBackupPricing provides pricing information for server backups.
type ServerBackupPricing struct {
Percentage string
}
// ServerTypePricing provides pricing information for a server type.
type ServerTypePricing struct {
ServerType *ServerType
Pricings []ServerTypeLocationPricing
}
// ServerTypeLocationPricing provides pricing information for a server type
// at a location.
type ServerTypeLocationPricing struct {
Location *Location
Hourly Price
Monthly Price
}
// LoadBalancerTypePricing provides pricing information for a Load Balancer type.
type LoadBalancerTypePricing struct {
LoadBalancerType *LoadBalancerType
Pricings []LoadBalancerTypeLocationPricing
}
// LoadBalancerTypeLocationPricing provides pricing information for a Load Balancer type
// at a location.
type LoadBalancerTypeLocationPricing struct {
Location *Location
Hourly Price
Monthly Price
}
// PricingClient is a client for the pricing API.
type PricingClient struct {
client *Client
}
// Get retrieves pricing information.
func (c *PricingClient) Get(ctx context.Context) (Pricing, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", "/pricing", nil)
if err != nil {
return Pricing{}, nil, err
}
var body schema.PricingGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return Pricing{}, nil, err
}
return PricingFromSchema(body.Pricing), resp, nil
}

View file

@ -0,0 +1,891 @@
package hcloud
import (
"net"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// This file provides converter functions to convert models in the
// schema package to models in the hcloud package and vice versa.
// ActionFromSchema converts a schema.Action to an Action.
func ActionFromSchema(s schema.Action) *Action {
action := &Action{
ID: s.ID,
Status: ActionStatus(s.Status),
Command: s.Command,
Progress: s.Progress,
Started: s.Started,
Resources: []*ActionResource{},
}
if s.Finished != nil {
action.Finished = *s.Finished
}
if s.Error != nil {
action.ErrorCode = s.Error.Code
action.ErrorMessage = s.Error.Message
}
for _, r := range s.Resources {
action.Resources = append(action.Resources, &ActionResource{
ID: r.ID,
Type: ActionResourceType(r.Type),
})
}
return action
}
// ActionsFromSchema converts a slice of schema.Action to a slice of Action.
func ActionsFromSchema(s []schema.Action) []*Action {
var actions []*Action
for _, a := range s {
actions = append(actions, ActionFromSchema(a))
}
return actions
}
// FloatingIPFromSchema converts a schema.FloatingIP to a FloatingIP.
func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP {
f := &FloatingIP{
ID: s.ID,
Type: FloatingIPType(s.Type),
HomeLocation: LocationFromSchema(s.HomeLocation),
Created: s.Created,
Blocked: s.Blocked,
Protection: FloatingIPProtection{
Delete: s.Protection.Delete,
},
Name: s.Name,
}
if s.Description != nil {
f.Description = *s.Description
}
if s.Server != nil {
f.Server = &Server{ID: *s.Server}
}
if f.Type == FloatingIPTypeIPv4 {
f.IP = net.ParseIP(s.IP)
} else {
f.IP, f.Network, _ = net.ParseCIDR(s.IP)
}
f.DNSPtr = map[string]string{}
for _, entry := range s.DNSPtr {
f.DNSPtr[entry.IP] = entry.DNSPtr
}
f.Labels = map[string]string{}
for key, value := range s.Labels {
f.Labels[key] = value
}
return f
}
// ISOFromSchema converts a schema.ISO to an ISO.
func ISOFromSchema(s schema.ISO) *ISO {
return &ISO{
ID: s.ID,
Name: s.Name,
Description: s.Description,
Type: ISOType(s.Type),
Deprecated: s.Deprecated,
}
}
// LocationFromSchema converts a schema.Location to a Location.
func LocationFromSchema(s schema.Location) *Location {
return &Location{
ID: s.ID,
Name: s.Name,
Description: s.Description,
Country: s.Country,
City: s.City,
Latitude: s.Latitude,
Longitude: s.Longitude,
NetworkZone: NetworkZone(s.NetworkZone),
}
}
// DatacenterFromSchema converts a schema.Datacenter to a Datacenter.
func DatacenterFromSchema(s schema.Datacenter) *Datacenter {
d := &Datacenter{
ID: s.ID,
Name: s.Name,
Description: s.Description,
Location: LocationFromSchema(s.Location),
ServerTypes: DatacenterServerTypes{
Available: []*ServerType{},
Supported: []*ServerType{},
},
}
for _, t := range s.ServerTypes.Available {
d.ServerTypes.Available = append(d.ServerTypes.Available, &ServerType{ID: t})
}
for _, t := range s.ServerTypes.Supported {
d.ServerTypes.Supported = append(d.ServerTypes.Supported, &ServerType{ID: t})
}
return d
}
// ServerFromSchema converts a schema.Server to a Server.
func ServerFromSchema(s schema.Server) *Server {
server := &Server{
ID: s.ID,
Name: s.Name,
Status: ServerStatus(s.Status),
Created: s.Created,
PublicNet: ServerPublicNetFromSchema(s.PublicNet),
ServerType: ServerTypeFromSchema(s.ServerType),
IncludedTraffic: s.IncludedTraffic,
RescueEnabled: s.RescueEnabled,
Datacenter: DatacenterFromSchema(s.Datacenter),
Locked: s.Locked,
Protection: ServerProtection{
Delete: s.Protection.Delete,
Rebuild: s.Protection.Rebuild,
},
}
if s.Image != nil {
server.Image = ImageFromSchema(*s.Image)
}
if s.BackupWindow != nil {
server.BackupWindow = *s.BackupWindow
}
if s.OutgoingTraffic != nil {
server.OutgoingTraffic = *s.OutgoingTraffic
}
if s.IngoingTraffic != nil {
server.IngoingTraffic = *s.IngoingTraffic
}
if s.ISO != nil {
server.ISO = ISOFromSchema(*s.ISO)
}
server.Labels = map[string]string{}
for key, value := range s.Labels {
server.Labels[key] = value
}
for _, id := range s.Volumes {
server.Volumes = append(server.Volumes, &Volume{ID: id})
}
for _, privNet := range s.PrivateNet {
server.PrivateNet = append(server.PrivateNet, ServerPrivateNetFromSchema(privNet))
}
return server
}
// ServerPublicNetFromSchema converts a schema.ServerPublicNet to a ServerPublicNet.
func ServerPublicNetFromSchema(s schema.ServerPublicNet) ServerPublicNet {
publicNet := ServerPublicNet{
IPv4: ServerPublicNetIPv4FromSchema(s.IPv4),
IPv6: ServerPublicNetIPv6FromSchema(s.IPv6),
}
for _, id := range s.FloatingIPs {
publicNet.FloatingIPs = append(publicNet.FloatingIPs, &FloatingIP{ID: id})
}
return publicNet
}
// ServerPublicNetIPv4FromSchema converts a schema.ServerPublicNetIPv4 to
// a ServerPublicNetIPv4.
func ServerPublicNetIPv4FromSchema(s schema.ServerPublicNetIPv4) ServerPublicNetIPv4 {
return ServerPublicNetIPv4{
IP: net.ParseIP(s.IP),
Blocked: s.Blocked,
DNSPtr: s.DNSPtr,
}
}
// ServerPublicNetIPv6FromSchema converts a schema.ServerPublicNetIPv6 to
// a ServerPublicNetIPv6.
func ServerPublicNetIPv6FromSchema(s schema.ServerPublicNetIPv6) ServerPublicNetIPv6 {
ipv6 := ServerPublicNetIPv6{
Blocked: s.Blocked,
DNSPtr: map[string]string{},
}
ipv6.IP, ipv6.Network, _ = net.ParseCIDR(s.IP)
for _, dnsPtr := range s.DNSPtr {
ipv6.DNSPtr[dnsPtr.IP] = dnsPtr.DNSPtr
}
return ipv6
}
// ServerPrivateNetFromSchema converts a schema.ServerPrivateNet to a ServerPrivateNet.
func ServerPrivateNetFromSchema(s schema.ServerPrivateNet) ServerPrivateNet {
n := ServerPrivateNet{
Network: &Network{ID: s.Network},
IP: net.ParseIP(s.IP),
MACAddress: s.MACAddress,
}
for _, ip := range s.AliasIPs {
n.Aliases = append(n.Aliases, net.ParseIP(ip))
}
return n
}
// ServerTypeFromSchema converts a schema.ServerType to a ServerType.
func ServerTypeFromSchema(s schema.ServerType) *ServerType {
st := &ServerType{
ID: s.ID,
Name: s.Name,
Description: s.Description,
Cores: s.Cores,
Memory: s.Memory,
Disk: s.Disk,
StorageType: StorageType(s.StorageType),
CPUType: CPUType(s.CPUType),
}
for _, price := range s.Prices {
st.Pricings = append(st.Pricings, ServerTypeLocationPricing{
Location: &Location{Name: price.Location},
Hourly: Price{
Net: price.PriceHourly.Net,
Gross: price.PriceHourly.Gross,
},
Monthly: Price{
Net: price.PriceMonthly.Net,
Gross: price.PriceMonthly.Gross,
},
})
}
return st
}
// SSHKeyFromSchema converts a schema.SSHKey to a SSHKey.
func SSHKeyFromSchema(s schema.SSHKey) *SSHKey {
sshKey := &SSHKey{
ID: s.ID,
Name: s.Name,
Fingerprint: s.Fingerprint,
PublicKey: s.PublicKey,
Created: s.Created,
}
sshKey.Labels = map[string]string{}
for key, value := range s.Labels {
sshKey.Labels[key] = value
}
return sshKey
}
// ImageFromSchema converts a schema.Image to an Image.
func ImageFromSchema(s schema.Image) *Image {
i := &Image{
ID: s.ID,
Type: ImageType(s.Type),
Status: ImageStatus(s.Status),
Description: s.Description,
DiskSize: s.DiskSize,
Created: s.Created,
RapidDeploy: s.RapidDeploy,
OSFlavor: s.OSFlavor,
Protection: ImageProtection{
Delete: s.Protection.Delete,
},
Deprecated: s.Deprecated,
}
if s.Name != nil {
i.Name = *s.Name
}
if s.ImageSize != nil {
i.ImageSize = *s.ImageSize
}
if s.OSVersion != nil {
i.OSVersion = *s.OSVersion
}
if s.CreatedFrom != nil {
i.CreatedFrom = &Server{
ID: s.CreatedFrom.ID,
Name: s.CreatedFrom.Name,
}
}
if s.BoundTo != nil {
i.BoundTo = &Server{
ID: *s.BoundTo,
}
}
i.Labels = map[string]string{}
for key, value := range s.Labels {
i.Labels[key] = value
}
return i
}
// VolumeFromSchema converts a schema.Volume to a Volume.
func VolumeFromSchema(s schema.Volume) *Volume {
v := &Volume{
ID: s.ID,
Name: s.Name,
Location: LocationFromSchema(s.Location),
Size: s.Size,
Status: VolumeStatus(s.Status),
LinuxDevice: s.LinuxDevice,
Protection: VolumeProtection{
Delete: s.Protection.Delete,
},
Created: s.Created,
}
if s.Server != nil {
v.Server = &Server{ID: *s.Server}
}
v.Labels = map[string]string{}
for key, value := range s.Labels {
v.Labels[key] = value
}
return v
}
// NetworkFromSchema converts a schema.Network to a Network.
func NetworkFromSchema(s schema.Network) *Network {
n := &Network{
ID: s.ID,
Name: s.Name,
Created: s.Created,
Protection: NetworkProtection{
Delete: s.Protection.Delete,
},
Labels: map[string]string{},
}
_, n.IPRange, _ = net.ParseCIDR(s.IPRange)
for _, subnet := range s.Subnets {
n.Subnets = append(n.Subnets, NetworkSubnetFromSchema(subnet))
}
for _, route := range s.Routes {
n.Routes = append(n.Routes, NetworkRouteFromSchema(route))
}
for _, serverID := range s.Servers {
n.Servers = append(n.Servers, &Server{ID: serverID})
}
for key, value := range s.Labels {
n.Labels[key] = value
}
return n
}
// NetworkSubnetFromSchema converts a schema.NetworkSubnet to a NetworkSubnet.
func NetworkSubnetFromSchema(s schema.NetworkSubnet) NetworkSubnet {
sn := NetworkSubnet{
Type: NetworkSubnetType(s.Type),
NetworkZone: NetworkZone(s.NetworkZone),
Gateway: net.ParseIP(s.Gateway),
}
_, sn.IPRange, _ = net.ParseCIDR(s.IPRange)
return sn
}
// NetworkRouteFromSchema converts a schema.NetworkRoute to a NetworkRoute.
func NetworkRouteFromSchema(s schema.NetworkRoute) NetworkRoute {
r := NetworkRoute{
Gateway: net.ParseIP(s.Gateway),
}
_, r.Destination, _ = net.ParseCIDR(s.Destination)
return r
}
// LoadBalancerTypeFromSchema converts a schema.LoadBalancerType to a LoadBalancerType.
func LoadBalancerTypeFromSchema(s schema.LoadBalancerType) *LoadBalancerType {
lt := &LoadBalancerType{
ID: s.ID,
Name: s.Name,
Description: s.Description,
MaxConnections: s.MaxConnections,
MaxServices: s.MaxServices,
MaxTargets: s.MaxTargets,
MaxAssignedCertificates: s.MaxAssignedCertificates,
}
for _, price := range s.Prices {
lt.Pricings = append(lt.Pricings, LoadBalancerTypeLocationPricing{
Location: &Location{Name: price.Location},
Hourly: Price{
Net: price.PriceHourly.Net,
Gross: price.PriceHourly.Gross,
},
Monthly: Price{
Net: price.PriceMonthly.Net,
Gross: price.PriceMonthly.Gross,
},
})
}
return lt
}
// LoadBalancerFromSchema converts a schema.LoadBalancer to a LoadBalancer.
func LoadBalancerFromSchema(s schema.LoadBalancer) *LoadBalancer {
l := &LoadBalancer{
ID: s.ID,
Name: s.Name,
PublicNet: LoadBalancerPublicNet{
Enabled: s.PublicNet.Enabled,
IPv4: LoadBalancerPublicNetIPv4{
IP: net.ParseIP(s.PublicNet.IPv4.IP),
},
IPv6: LoadBalancerPublicNetIPv6{
IP: net.ParseIP(s.PublicNet.IPv6.IP),
},
},
Location: LocationFromSchema(s.Location),
LoadBalancerType: LoadBalancerTypeFromSchema(s.LoadBalancerType),
Algorithm: LoadBalancerAlgorithm{Type: LoadBalancerAlgorithmType(s.Algorithm.Type)},
Protection: LoadBalancerProtection{
Delete: s.Protection.Delete,
},
Labels: map[string]string{},
Created: s.Created,
IncludedTraffic: s.IncludedTraffic,
}
for _, privateNet := range s.PrivateNet {
l.PrivateNet = append(l.PrivateNet, LoadBalancerPrivateNet{
Network: &Network{ID: privateNet.Network},
IP: net.ParseIP(privateNet.IP),
})
}
if s.OutgoingTraffic != nil {
l.OutgoingTraffic = *s.OutgoingTraffic
}
if s.IngoingTraffic != nil {
l.IngoingTraffic = *s.IngoingTraffic
}
for _, service := range s.Services {
l.Services = append(l.Services, LoadBalancerServiceFromSchema(service))
}
for _, target := range s.Targets {
l.Targets = append(l.Targets, LoadBalancerTargetFromSchema(target))
}
for key, value := range s.Labels {
l.Labels[key] = value
}
return l
}
// LoadBalancerServiceFromSchema converts a schema.LoadBalancerService to a LoadBalancerService.
func LoadBalancerServiceFromSchema(s schema.LoadBalancerService) LoadBalancerService {
ls := LoadBalancerService{
Protocol: LoadBalancerServiceProtocol(s.Protocol),
ListenPort: s.ListenPort,
DestinationPort: s.DestinationPort,
Proxyprotocol: s.Proxyprotocol,
HealthCheck: LoadBalancerServiceHealthCheckFromSchema(s.HealthCheck),
}
if s.HTTP != nil {
ls.HTTP = LoadBalancerServiceHTTP{
CookieName: s.HTTP.CookieName,
CookieLifetime: time.Duration(s.HTTP.CookieLifetime) * time.Second,
RedirectHTTP: s.HTTP.RedirectHTTP,
StickySessions: s.HTTP.StickySessions,
}
for _, certificateID := range s.HTTP.Certificates {
ls.HTTP.Certificates = append(ls.HTTP.Certificates, &Certificate{ID: certificateID})
}
}
return ls
}
// LoadBalancerServiceHealthCheckFromSchema converts a schema.LoadBalancerServiceHealthCheck to a LoadBalancerServiceHealthCheck.
func LoadBalancerServiceHealthCheckFromSchema(s *schema.LoadBalancerServiceHealthCheck) LoadBalancerServiceHealthCheck {
lsh := LoadBalancerServiceHealthCheck{
Protocol: LoadBalancerServiceProtocol(s.Protocol),
Port: s.Port,
Interval: time.Duration(s.Interval) * time.Second,
Retries: s.Retries,
Timeout: time.Duration(s.Timeout) * time.Second,
}
if s.HTTP != nil {
lsh.HTTP = &LoadBalancerServiceHealthCheckHTTP{
Domain: s.HTTP.Domain,
Path: s.HTTP.Path,
Response: s.HTTP.Response,
StatusCodes: s.HTTP.StatusCodes,
TLS: s.HTTP.TLS,
}
}
return lsh
}
// LoadBalancerTargetFromSchema converts a schema.LoadBalancerTarget to a LoadBalancerTarget.
func LoadBalancerTargetFromSchema(s schema.LoadBalancerTarget) LoadBalancerTarget {
lt := LoadBalancerTarget{
Type: LoadBalancerTargetType(s.Type),
UsePrivateIP: s.UsePrivateIP,
}
if s.Server != nil {
lt.Server = &LoadBalancerTargetServer{
Server: &Server{ID: s.Server.ID},
}
}
if s.LabelSelector != nil {
lt.LabelSelector = &LoadBalancerTargetLabelSelector{
Selector: s.LabelSelector.Selector,
}
}
if s.IP != nil {
lt.IP = &LoadBalancerTargetIP{IP: s.IP.IP}
}
for _, healthStatus := range s.HealthStatus {
lt.HealthStatus = append(lt.HealthStatus, LoadBalancerTargetHealthStatusFromSchema(healthStatus))
}
for _, target := range s.Targets {
lt.Targets = append(lt.Targets, LoadBalancerTargetFromSchema(target))
}
return lt
}
// LoadBalancerTargetHealthStatusFromSchema converts a schema.LoadBalancerTarget to a LoadBalancerTarget.
func LoadBalancerTargetHealthStatusFromSchema(s schema.LoadBalancerTargetHealthStatus) LoadBalancerTargetHealthStatus {
return LoadBalancerTargetHealthStatus{
ListenPort: s.ListenPort,
Status: LoadBalancerTargetHealthStatusStatus(s.Status),
}
}
// CertificateFromSchema converts a schema.Certificate to a Certificate.
func CertificateFromSchema(s schema.Certificate) *Certificate {
c := &Certificate{
ID: s.ID,
Name: s.Name,
Certificate: s.Certificate,
Created: s.Created,
NotValidBefore: s.NotValidBefore,
NotValidAfter: s.NotValidAfter,
DomainNames: s.DomainNames,
Fingerprint: s.Fingerprint,
}
if len(s.Labels) > 0 {
c.Labels = make(map[string]string)
}
for key, value := range s.Labels {
c.Labels[key] = value
}
return c
}
// PaginationFromSchema converts a schema.MetaPagination to a Pagination.
func PaginationFromSchema(s schema.MetaPagination) Pagination {
return Pagination{
Page: s.Page,
PerPage: s.PerPage,
PreviousPage: s.PreviousPage,
NextPage: s.NextPage,
LastPage: s.LastPage,
TotalEntries: s.TotalEntries,
}
}
// ErrorFromSchema converts a schema.Error to an Error.
func ErrorFromSchema(s schema.Error) Error {
e := Error{
Code: ErrorCode(s.Code),
Message: s.Message,
}
switch d := s.Details.(type) {
case schema.ErrorDetailsInvalidInput:
details := ErrorDetailsInvalidInput{
Fields: []ErrorDetailsInvalidInputField{},
}
for _, field := range d.Fields {
details.Fields = append(details.Fields, ErrorDetailsInvalidInputField{
Name: field.Name,
Messages: field.Messages,
})
}
e.Details = details
}
return e
}
// PricingFromSchema converts a schema.Pricing to a Pricing.
func PricingFromSchema(s schema.Pricing) Pricing {
p := Pricing{
Image: ImagePricing{
PerGBMonth: Price{
Currency: s.Currency,
VATRate: s.VATRate,
Net: s.Image.PricePerGBMonth.Net,
Gross: s.Image.PricePerGBMonth.Gross,
},
},
FloatingIP: FloatingIPPricing{
Monthly: Price{
Currency: s.Currency,
VATRate: s.VATRate,
Net: s.FloatingIP.PriceMonthly.Net,
Gross: s.FloatingIP.PriceMonthly.Gross,
},
},
Traffic: TrafficPricing{
PerTB: Price{
Currency: s.Currency,
VATRate: s.VATRate,
Net: s.Traffic.PricePerTB.Net,
Gross: s.Traffic.PricePerTB.Gross,
},
},
ServerBackup: ServerBackupPricing{
Percentage: s.ServerBackup.Percentage,
},
}
for _, serverType := range s.ServerTypes {
var pricings []ServerTypeLocationPricing
for _, price := range serverType.Prices {
pricings = append(pricings, ServerTypeLocationPricing{
Location: &Location{Name: price.Location},
Hourly: Price{
Currency: s.Currency,
VATRate: s.VATRate,
Net: price.PriceHourly.Net,
Gross: price.PriceHourly.Gross,
},
Monthly: Price{
Currency: s.Currency,
VATRate: s.VATRate,
Net: price.PriceMonthly.Net,
Gross: price.PriceMonthly.Gross,
},
})
}
p.ServerTypes = append(p.ServerTypes, ServerTypePricing{
ServerType: &ServerType{
ID: serverType.ID,
Name: serverType.Name,
},
Pricings: pricings,
})
}
for _, loadBalancerType := range s.LoadBalancerTypes {
var pricings []LoadBalancerTypeLocationPricing
for _, price := range loadBalancerType.Prices {
pricings = append(pricings, LoadBalancerTypeLocationPricing{
Location: &Location{Name: price.Location},
Hourly: Price{
Currency: s.Currency,
VATRate: s.VATRate,
Net: price.PriceHourly.Net,
Gross: price.PriceHourly.Gross,
},
Monthly: Price{
Currency: s.Currency,
VATRate: s.VATRate,
Net: price.PriceMonthly.Net,
Gross: price.PriceMonthly.Gross,
},
})
}
p.LoadBalancerTypes = append(p.LoadBalancerTypes, LoadBalancerTypePricing{
LoadBalancerType: &LoadBalancerType{
ID: loadBalancerType.ID,
Name: loadBalancerType.Name,
},
Pricings: pricings,
})
}
return p
}
func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBalancerCreateRequest {
req := schema.LoadBalancerCreateRequest{
Name: opts.Name,
PublicInterface: opts.PublicInterface,
}
if opts.Algorithm != nil {
req.Algorithm = &schema.LoadBalancerCreateRequestAlgorithm{
Type: string(opts.Algorithm.Type),
}
}
if opts.LoadBalancerType.ID != 0 {
req.LoadBalancerType = opts.LoadBalancerType.ID
} else if opts.LoadBalancerType.Name != "" {
req.LoadBalancerType = opts.LoadBalancerType.Name
}
if opts.Location != nil {
if opts.Location.ID != 0 {
req.Location = String(strconv.Itoa(opts.Location.ID))
} else {
req.Location = String(opts.Location.Name)
}
}
if opts.NetworkZone != "" {
req.NetworkZone = String(string(opts.NetworkZone))
}
if opts.Labels != nil {
req.Labels = &opts.Labels
}
if opts.Network != nil {
req.Network = Int(opts.Network.ID)
}
for _, target := range opts.Targets {
schemaTarget := schema.LoadBalancerCreateRequestTarget{}
switch target.Type {
case LoadBalancerTargetTypeServer:
schemaTarget.Type = string(LoadBalancerTargetTypeServer)
schemaTarget.Server = &schema.LoadBalancerCreateRequestTargetServer{ID: target.Server.Server.ID}
case LoadBalancerTargetTypeLabelSelector:
schemaTarget.Type = string(LoadBalancerTargetTypeLabelSelector)
schemaTarget.LabelSelector = &schema.LoadBalancerCreateRequestTargetLabelSelector{Selector: target.LabelSelector.Selector}
case LoadBalancerTargetTypeIP:
schemaTarget.Type = string(LoadBalancerTargetTypeIP)
schemaTarget.IP = &schema.LoadBalancerCreateRequestTargetIP{IP: target.IP.IP}
}
req.Targets = append(req.Targets, schemaTarget)
}
for _, service := range opts.Services {
schemaService := schema.LoadBalancerCreateRequestService{
Protocol: string(service.Protocol),
ListenPort: service.ListenPort,
DestinationPort: service.DestinationPort,
Proxyprotocol: service.Proxyprotocol,
}
if service.HTTP != nil {
schemaService.HTTP = &schema.LoadBalancerCreateRequestServiceHTTP{
RedirectHTTP: service.HTTP.RedirectHTTP,
StickySessions: service.HTTP.StickySessions,
CookieName: service.HTTP.CookieName,
}
if sec := service.HTTP.CookieLifetime.Seconds(); sec != 0 {
schemaService.HTTP.CookieLifetime = Int(int(sec))
}
if service.HTTP.Certificates != nil {
certificates := []int{}
for _, certificate := range service.HTTP.Certificates {
certificates = append(certificates, certificate.ID)
}
schemaService.HTTP.Certificates = &certificates
}
}
if service.HealthCheck != nil {
schemaHealthCheck := &schema.LoadBalancerCreateRequestServiceHealthCheck{
Protocol: string(service.HealthCheck.Protocol),
Port: service.HealthCheck.Port,
Retries: service.HealthCheck.Retries,
}
if service.HealthCheck.Interval != nil {
schemaHealthCheck.Interval = Int(int(service.HealthCheck.Interval.Seconds()))
}
if service.HealthCheck.Timeout != nil {
schemaHealthCheck.Timeout = Int(int(service.HealthCheck.Timeout.Seconds()))
}
if service.HealthCheck.HTTP != nil {
schemaHealthCheckHTTP := &schema.LoadBalancerCreateRequestServiceHealthCheckHTTP{
Domain: service.HealthCheck.HTTP.Domain,
Path: service.HealthCheck.HTTP.Path,
Response: service.HealthCheck.HTTP.Response,
TLS: service.HealthCheck.HTTP.TLS,
}
if service.HealthCheck.HTTP.StatusCodes != nil {
schemaHealthCheckHTTP.StatusCodes = &service.HealthCheck.HTTP.StatusCodes
}
schemaHealthCheck.HTTP = schemaHealthCheckHTTP
}
schemaService.HealthCheck = schemaHealthCheck
}
req.Services = append(req.Services, schemaService)
}
return req
}
func loadBalancerAddServiceOptsToSchema(opts LoadBalancerAddServiceOpts) schema.LoadBalancerActionAddServiceRequest {
req := schema.LoadBalancerActionAddServiceRequest{
Protocol: string(opts.Protocol),
ListenPort: opts.ListenPort,
DestinationPort: opts.DestinationPort,
Proxyprotocol: opts.Proxyprotocol,
}
if opts.HTTP != nil {
req.HTTP = &schema.LoadBalancerActionAddServiceRequestHTTP{
CookieName: opts.HTTP.CookieName,
RedirectHTTP: opts.HTTP.RedirectHTTP,
StickySessions: opts.HTTP.StickySessions,
}
if opts.HTTP.CookieLifetime != nil {
req.HTTP.CookieLifetime = Int(int(opts.HTTP.CookieLifetime.Seconds()))
}
if opts.HTTP.Certificates != nil {
certificates := []int{}
for _, certificate := range opts.HTTP.Certificates {
certificates = append(certificates, certificate.ID)
}
req.HTTP.Certificates = &certificates
}
}
if opts.HealthCheck != nil {
req.HealthCheck = &schema.LoadBalancerActionAddServiceRequestHealthCheck{
Protocol: string(opts.HealthCheck.Protocol),
Port: opts.HealthCheck.Port,
Retries: opts.HealthCheck.Retries,
}
if opts.HealthCheck.Interval != nil {
req.HealthCheck.Interval = Int(int(opts.HealthCheck.Interval.Seconds()))
}
if opts.HealthCheck.Timeout != nil {
req.HealthCheck.Timeout = Int(int(opts.HealthCheck.Timeout.Seconds()))
}
if opts.HealthCheck.HTTP != nil {
req.HealthCheck.HTTP = &schema.LoadBalancerActionAddServiceRequestHealthCheckHTTP{
Domain: opts.HealthCheck.HTTP.Domain,
Path: opts.HealthCheck.HTTP.Path,
Response: opts.HealthCheck.HTTP.Response,
TLS: opts.HealthCheck.HTTP.TLS,
}
if opts.HealthCheck.HTTP.StatusCodes != nil {
req.HealthCheck.HTTP.StatusCodes = &opts.HealthCheck.HTTP.StatusCodes
}
}
}
return req
}
func loadBalancerUpdateServiceOptsToSchema(opts LoadBalancerUpdateServiceOpts) schema.LoadBalancerActionUpdateServiceRequest {
req := schema.LoadBalancerActionUpdateServiceRequest{
DestinationPort: opts.DestinationPort,
Proxyprotocol: opts.Proxyprotocol,
}
if opts.Protocol != "" {
req.Protocol = String(string(opts.Protocol))
}
if opts.HTTP != nil {
req.HTTP = &schema.LoadBalancerActionUpdateServiceRequestHTTP{
CookieName: opts.HTTP.CookieName,
RedirectHTTP: opts.HTTP.RedirectHTTP,
StickySessions: opts.HTTP.StickySessions,
}
if opts.HTTP.CookieLifetime != nil {
req.HTTP.CookieLifetime = Int(int(opts.HTTP.CookieLifetime.Seconds()))
}
if opts.HTTP.Certificates != nil {
certificates := []int{}
for _, certificate := range opts.HTTP.Certificates {
certificates = append(certificates, certificate.ID)
}
req.HTTP.Certificates = &certificates
}
}
if opts.HealthCheck != nil {
req.HealthCheck = &schema.LoadBalancerActionUpdateServiceRequestHealthCheck{
Port: opts.HealthCheck.Port,
Retries: opts.HealthCheck.Retries,
}
if opts.HealthCheck.Interval != nil {
req.HealthCheck.Interval = Int(int(opts.HealthCheck.Interval.Seconds()))
}
if opts.HealthCheck.Timeout != nil {
req.HealthCheck.Timeout = Int(int(opts.HealthCheck.Timeout.Seconds()))
}
if opts.HealthCheck.Protocol != "" {
req.HealthCheck.Protocol = String(string(opts.HealthCheck.Protocol))
}
if opts.HealthCheck.HTTP != nil {
req.HealthCheck.HTTP = &schema.LoadBalancerActionUpdateServiceRequestHealthCheckHTTP{
Domain: opts.HealthCheck.HTTP.Domain,
Path: opts.HealthCheck.HTTP.Path,
Response: opts.HealthCheck.HTTP.Response,
TLS: opts.HealthCheck.HTTP.TLS,
}
if opts.HealthCheck.HTTP.StatusCodes != nil {
req.HealthCheck.HTTP.StatusCodes = &opts.HealthCheck.HTTP.StatusCodes
}
}
}
return req
}

View file

@ -0,0 +1,39 @@
package schema
import "time"
// Action defines the schema of an action.
type Action struct {
ID int `json:"id"`
Status string `json:"status"`
Command string `json:"command"`
Progress int `json:"progress"`
Started time.Time `json:"started"`
Finished *time.Time `json:"finished"`
Error *ActionError `json:"error"`
Resources []ActionResourceReference `json:"resources"`
}
// ActionResourceReference defines the schema of an action resource reference.
type ActionResourceReference struct {
ID int `json:"id"`
Type string `json:"type"`
}
// ActionError defines the schema of an error embedded
// in an action.
type ActionError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// ActionGetResponse is the schema of the response when
// retrieving a single action.
type ActionGetResponse struct {
Action Action `json:"action"`
}
// ActionListResponse defines the schema of the response when listing actions.
type ActionListResponse struct {
Actions []Action `json:"actions"`
}

View file

@ -0,0 +1,52 @@
package schema
import "time"
// Certificate defines the schema of an certificate.
type Certificate struct {
ID int `json:"id"`
Name string `json:"name"`
Labels map[string]string `json:"labels"`
Certificate string `json:"certificate"`
Created time.Time `json:"created"`
NotValidBefore time.Time `json:"not_valid_before"`
NotValidAfter time.Time `json:"not_valid_after"`
DomainNames []string `json:"domain_names"`
Fingerprint string `json:"fingerprint"`
}
// CertificateListResponse defines the schema of the response when
// listing Certificates.
type CertificateListResponse struct {
Certificates []Certificate `json:"certificates"`
}
// CertificateGetResponse defines the schema of the response when
// retrieving a single Certificate.
type CertificateGetResponse struct {
Certificate Certificate `json:"certificate"`
}
// CertificateCreateRequest defines the schema of the request to create a certificate.
type CertificateCreateRequest struct {
Name string `json:"name"`
Certificate string `json:"certificate"`
PrivateKey string `json:"private_key"`
Labels *map[string]string `json:"labels,omitempty"`
}
// CertificateCreateResponse defines the schema of the response when creating a certificate.
type CertificateCreateResponse struct {
Certificate Certificate `json:"certificate"`
}
// CertificateUpdateRequest defines the schema of the request to update a certificate.
type CertificateUpdateRequest struct {
Name *string `json:"name,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
// CertificateUpdateResponse defines the schema of the response when updating a certificate.
type CertificateUpdateResponse struct {
Certificate Certificate `json:"certificate"`
}

View file

@ -0,0 +1,23 @@
package schema
// Datacenter defines the schema of a datacenter.
type Datacenter struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Location Location `json:"location"`
ServerTypes struct {
Supported []int `json:"supported"`
Available []int `json:"available"`
} `json:"server_types"`
}
// DatacenterGetResponse defines the schema of the response when retrieving a single datacenter.
type DatacenterGetResponse struct {
Datacenter Datacenter `json:"datacenter"`
}
// DatacenterListResponse defines the schema of the response when listing datacenters.
type DatacenterListResponse struct {
Datacenters []Datacenter `json:"datacenters"`
}

View file

@ -0,0 +1,43 @@
package schema
import "encoding/json"
// Error represents the schema of an error response.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
DetailsRaw json.RawMessage `json:"details"`
Details interface{}
}
// UnmarshalJSON overrides default json unmarshalling.
func (e *Error) UnmarshalJSON(data []byte) (err error) {
type Alias Error
alias := (*Alias)(e)
if err = json.Unmarshal(data, alias); err != nil {
return
}
switch e.Code {
case "invalid_input":
details := ErrorDetailsInvalidInput{}
if err = json.Unmarshal(e.DetailsRaw, &details); err != nil {
return
}
alias.Details = details
}
return
}
// ErrorResponse defines the schema of a response containing an error.
type ErrorResponse struct {
Error Error `json:"error"`
}
// ErrorDetailsInvalidInput defines the schema of the Details field
// of an error with code 'invalid_input'.
type ErrorDetailsInvalidInput struct {
Fields []struct {
Name string `json:"name"`
Messages []string `json:"messages"`
} `json:"fields"`
}

View file

@ -0,0 +1,118 @@
package schema
import "time"
// FloatingIP defines the schema of a Floating IP.
type FloatingIP struct {
ID int `json:"id"`
Description *string `json:"description"`
Created time.Time `json:"created"`
IP string `json:"ip"`
Type string `json:"type"`
Server *int `json:"server"`
DNSPtr []FloatingIPDNSPtr `json:"dns_ptr"`
HomeLocation Location `json:"home_location"`
Blocked bool `json:"blocked"`
Protection FloatingIPProtection `json:"protection"`
Labels map[string]string `json:"labels"`
Name string `json:"name"`
}
// FloatingIPProtection represents the protection level of a Floating IP.
type FloatingIPProtection struct {
Delete bool `json:"delete"`
}
// FloatingIPDNSPtr contains reverse DNS information for a
// IPv4 or IPv6 Floating IP.
type FloatingIPDNSPtr struct {
IP string `json:"ip"`
DNSPtr string `json:"dns_ptr"`
}
// FloatingIPGetResponse defines the schema of the response when
// retrieving a single Floating IP.
type FloatingIPGetResponse struct {
FloatingIP FloatingIP `json:"floating_ip"`
}
// FloatingIPUpdateRequest defines the schema of the request to update a Floating IP.
type FloatingIPUpdateRequest struct {
Description string `json:"description,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
Name string `json:"name,omitempty"`
}
// FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP.
type FloatingIPUpdateResponse struct {
FloatingIP FloatingIP `json:"floating_ip"`
}
// FloatingIPListResponse defines the schema of the response when
// listing Floating IPs.
type FloatingIPListResponse struct {
FloatingIPs []FloatingIP `json:"floating_ips"`
}
// FloatingIPCreateRequest defines the schema of the request to
// create a Floating IP.
type FloatingIPCreateRequest struct {
Type string `json:"type"`
HomeLocation *string `json:"home_location,omitempty"`
Server *int `json:"server,omitempty"`
Description *string `json:"description,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
Name *string `json:"name,omitempty"`
}
// FloatingIPCreateResponse defines the schema of the response
// when creating a Floating IP.
type FloatingIPCreateResponse struct {
FloatingIP FloatingIP `json:"floating_ip"`
Action *Action `json:"action"`
}
// FloatingIPActionAssignRequest defines the schema of the request to
// create an assign Floating IP action.
type FloatingIPActionAssignRequest struct {
Server int `json:"server"`
}
// FloatingIPActionAssignResponse defines the schema of the response when
// creating an assign action.
type FloatingIPActionAssignResponse struct {
Action Action `json:"action"`
}
// FloatingIPActionUnassignRequest defines the schema of the request to
// create an unassign Floating IP action.
type FloatingIPActionUnassignRequest struct{}
// FloatingIPActionUnassignResponse defines the schema of the response when
// creating an unassign action.
type FloatingIPActionUnassignResponse struct {
Action Action `json:"action"`
}
// FloatingIPActionChangeDNSPtrRequest defines the schema for the request to
// change a Floating IP's reverse DNS pointer.
type FloatingIPActionChangeDNSPtrRequest struct {
IP string `json:"ip"`
DNSPtr *string `json:"dns_ptr"`
}
// FloatingIPActionChangeDNSPtrResponse defines the schema of the response when
// creating a change_dns_ptr Floating IP action.
type FloatingIPActionChangeDNSPtrResponse struct {
Action Action `json:"action"`
}
// FloatingIPActionChangeProtectionRequest defines the schema of the request to change the resource protection of a Floating IP.
type FloatingIPActionChangeProtectionRequest struct {
Delete *bool `json:"delete,omitempty"`
}
// FloatingIPActionChangeProtectionResponse defines the schema of the response when changing the resource protection of a Floating IP.
type FloatingIPActionChangeProtectionResponse struct {
Action Action `json:"action"`
}

View file

@ -0,0 +1,68 @@
package schema
import "time"
// Image defines the schema of an image.
type Image struct {
ID int `json:"id"`
Status string `json:"status"`
Type string `json:"type"`
Name *string `json:"name"`
Description string `json:"description"`
ImageSize *float32 `json:"image_size"`
DiskSize float32 `json:"disk_size"`
Created time.Time `json:"created"`
CreatedFrom *ImageCreatedFrom `json:"created_from"`
BoundTo *int `json:"bound_to"`
OSFlavor string `json:"os_flavor"`
OSVersion *string `json:"os_version"`
RapidDeploy bool `json:"rapid_deploy"`
Protection ImageProtection `json:"protection"`
Deprecated time.Time `json:"deprecated"`
Labels map[string]string `json:"labels"`
}
// ImageProtection represents the protection level of a image.
type ImageProtection struct {
Delete bool `json:"delete"`
}
// ImageCreatedFrom defines the schema of the images created from reference.
type ImageCreatedFrom struct {
ID int `json:"id"`
Name string `json:"name"`
}
// ImageGetResponse defines the schema of the response when
// retrieving a single image.
type ImageGetResponse struct {
Image Image `json:"image"`
}
// ImageListResponse defines the schema of the response when
// listing images.
type ImageListResponse struct {
Images []Image `json:"images"`
}
// ImageUpdateRequest defines the schema of the request to update an image.
type ImageUpdateRequest struct {
Description *string `json:"description,omitempty"`
Type *string `json:"type,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
// ImageUpdateResponse defines the schema of the response when updating an image.
type ImageUpdateResponse struct {
Image Image `json:"image"`
}
// ImageActionChangeProtectionRequest defines the schema of the request to change the resource protection of an image.
type ImageActionChangeProtectionRequest struct {
Delete *bool `json:"delete,omitempty"`
}
// ImageActionChangeProtectionResponse defines the schema of the response when changing the resource protection of an image.
type ImageActionChangeProtectionResponse struct {
Action Action `json:"action"`
}

View file

@ -0,0 +1,22 @@
package schema
import "time"
// ISO defines the schema of an ISO image.
type ISO struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
Deprecated time.Time `json:"deprecated"`
}
// ISOGetResponse defines the schema of the response when retrieving a single ISO.
type ISOGetResponse struct {
ISO ISO `json:"iso"`
}
// ISOListResponse defines the schema of the response when listing ISOs.
type ISOListResponse struct {
ISOs []ISO `json:"isos"`
}

View file

@ -0,0 +1,386 @@
package schema
import "time"
type LoadBalancer struct {
ID int `json:"id"`
Name string `json:"name"`
PublicNet LoadBalancerPublicNet `json:"public_net"`
PrivateNet []LoadBalancerPrivateNet `json:"private_net"`
Location Location `json:"location"`
LoadBalancerType LoadBalancerType `json:"load_balancer_type"`
Protection LoadBalancerProtection `json:"protection"`
Labels map[string]string `json:"labels"`
Created time.Time `json:"created"`
Services []LoadBalancerService `json:"services"`
Targets []LoadBalancerTarget `json:"targets"`
Algorithm LoadBalancerAlgorithm `json:"algorithm"`
IncludedTraffic uint64 `json:"included_traffic"`
OutgoingTraffic *uint64 `json:"outgoing_traffic"`
IngoingTraffic *uint64 `json:"ingoing_traffic"`
}
type LoadBalancerPublicNet struct {
Enabled bool `json:"enabled"`
IPv4 LoadBalancerPublicNetIPv4 `json:"ipv4"`
IPv6 LoadBalancerPublicNetIPv6 `json:"ipv6"`
}
type LoadBalancerPublicNetIPv4 struct {
IP string `json:"ip"`
}
type LoadBalancerPublicNetIPv6 struct {
IP string `json:"ip"`
}
type LoadBalancerPrivateNet struct {
Network int `json:"network"`
IP string `json:"ip"`
}
type LoadBalancerAlgorithm struct {
Type string `json:"type"`
}
type LoadBalancerProtection struct {
Delete bool `json:"delete"`
}
type LoadBalancerService struct {
Protocol string `json:"protocol"`
ListenPort int `json:"listen_port"`
DestinationPort int `json:"destination_port"`
Proxyprotocol bool `json:"proxyprotocol"`
HTTP *LoadBalancerServiceHTTP `json:"http"`
HealthCheck *LoadBalancerServiceHealthCheck `json:"health_check"`
}
type LoadBalancerServiceHTTP struct {
CookieName string `json:"cookie_name"`
CookieLifetime int `json:"cookie_lifetime"`
Certificates []int `json:"certificates"`
RedirectHTTP bool `json:"redirect_http"`
StickySessions bool `json:"sticky_sessions"`
}
type LoadBalancerServiceHealthCheck struct {
Protocol string `json:"protocol"`
Port int `json:"port"`
Interval int `json:"interval"`
Timeout int `json:"timeout"`
Retries int `json:"retries"`
HTTP *LoadBalancerServiceHealthCheckHTTP `json:"http"`
}
type LoadBalancerServiceHealthCheckHTTP struct {
Domain string `json:"domain"`
Path string `json:"path"`
Response string `json:"response"`
StatusCodes []string `json:"status_codes"`
TLS bool `json:"tls"`
}
type LoadBalancerTarget struct {
Type string `json:"type"`
Server *LoadBalancerTargetServer `json:"server"`
LabelSelector *LoadBalancerTargetLabelSelector `json:"label_selector"`
IP *LoadBalancerTargetIP `json:"ip"`
HealthStatus []LoadBalancerTargetHealthStatus `json:"health_status"`
UsePrivateIP bool `json:"use_private_ip"`
Targets []LoadBalancerTarget `json:"targets,omitempty"`
}
type LoadBalancerTargetHealthStatus struct {
ListenPort int `json:"listen_port"`
Status string `json:"status"`
}
type LoadBalancerTargetServer struct {
ID int `json:"id"`
}
type LoadBalancerTargetLabelSelector struct {
Selector string `json:"selector"`
}
type LoadBalancerTargetIP struct {
IP string `json:"ip"`
}
type LoadBalancerListResponse struct {
LoadBalancers []LoadBalancer `json:"load_balancers"`
}
type LoadBalancerGetResponse struct {
LoadBalancer LoadBalancer `json:"load_balancer"`
}
type LoadBalancerActionAddTargetRequest struct {
Type string `json:"type"`
Server *LoadBalancerActionAddTargetRequestServer `json:"server,omitempty"`
LabelSelector *LoadBalancerActionAddTargetRequestLabelSelector `json:"label_selector,omitempty"`
IP *LoadBalancerActionAddTargetRequestIP `json:"ip,omitempty"`
UsePrivateIP *bool `json:"use_private_ip,omitempty"`
}
type LoadBalancerActionAddTargetRequestServer struct {
ID int `json:"id"`
}
type LoadBalancerActionAddTargetRequestLabelSelector struct {
Selector string `json:"selector"`
}
type LoadBalancerActionAddTargetRequestIP struct {
IP string `json:"ip"`
}
type LoadBalancerActionAddTargetResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionRemoveTargetRequest struct {
Type string `json:"type"`
Server *LoadBalancerActionRemoveTargetRequestServer `json:"server,omitempty"`
LabelSelector *LoadBalancerActionRemoveTargetRequestLabelSelector `json:"label_selector,omitempty"`
IP *LoadBalancerActionRemoveTargetRequestIP `json:"ip,omitempty"`
}
type LoadBalancerActionRemoveTargetRequestServer struct {
ID int `json:"id"`
}
type LoadBalancerActionRemoveTargetRequestLabelSelector struct {
Selector string `json:"selector"`
}
type LoadBalancerActionRemoveTargetRequestIP struct {
IP string `json:"ip"`
}
type LoadBalancerActionRemoveTargetResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionAddServiceRequest struct {
Protocol string `json:"protocol"`
ListenPort *int `json:"listen_port,omitempty"`
DestinationPort *int `json:"destination_port,omitempty"`
Proxyprotocol *bool `json:"proxyprotocol,omitempty"`
HTTP *LoadBalancerActionAddServiceRequestHTTP `json:"http,omitempty"`
HealthCheck *LoadBalancerActionAddServiceRequestHealthCheck `json:"health_check,omitempty"`
}
type LoadBalancerActionAddServiceRequestHTTP struct {
CookieName *string `json:"cookie_name,omitempty"`
CookieLifetime *int `json:"cookie_lifetime,omitempty"`
Certificates *[]int `json:"certificates,omitempty"`
RedirectHTTP *bool `json:"redirect_http,omitempty"`
StickySessions *bool `json:"sticky_sessions,omitempty"`
}
type LoadBalancerActionAddServiceRequestHealthCheck struct {
Protocol string `json:"protocol"`
Port *int `json:"port,omitempty"`
Interval *int `json:"interval,omitempty"`
Timeout *int `json:"timeout,omitempty"`
Retries *int `json:"retries,omitempty"`
HTTP *LoadBalancerActionAddServiceRequestHealthCheckHTTP `json:"http,omitempty"`
}
type LoadBalancerActionAddServiceRequestHealthCheckHTTP struct {
Domain *string `json:"domain,omitempty"`
Path *string `json:"path,omitempty"`
Response *string `json:"response,omitempty"`
StatusCodes *[]string `json:"status_codes,omitempty"`
TLS *bool `json:"tls,omitempty"`
}
type LoadBalancerActionAddServiceResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionUpdateServiceRequest struct {
ListenPort int `json:"listen_port"`
Protocol *string `json:"protocol,omitempty"`
DestinationPort *int `json:"destination_port,omitempty"`
Proxyprotocol *bool `json:"proxyprotocol,omitempty"`
HTTP *LoadBalancerActionUpdateServiceRequestHTTP `json:"http,omitempty"`
HealthCheck *LoadBalancerActionUpdateServiceRequestHealthCheck `json:"health_check,omitempty"`
}
type LoadBalancerActionUpdateServiceRequestHTTP struct {
CookieName *string `json:"cookie_name,omitempty"`
CookieLifetime *int `json:"cookie_lifetime,omitempty"`
Certificates *[]int `json:"certificates,omitempty"`
RedirectHTTP *bool `json:"redirect_http,omitempty"`
StickySessions *bool `json:"sticky_sessions,omitempty"`
}
type LoadBalancerActionUpdateServiceRequestHealthCheck struct {
Protocol *string `json:"protocol,omitempty"`
Port *int `json:"port,omitempty"`
Interval *int `json:"interval,omitempty"`
Timeout *int `json:"timeout,omitempty"`
Retries *int `json:"retries,omitempty"`
HTTP *LoadBalancerActionUpdateServiceRequestHealthCheckHTTP `json:"http,omitempty"`
}
type LoadBalancerActionUpdateServiceRequestHealthCheckHTTP struct {
Domain *string `json:"domain,omitempty"`
Path *string `json:"path,omitempty"`
Response *string `json:"response,omitempty"`
StatusCodes *[]string `json:"status_codes,omitempty"`
TLS *bool `json:"tls,omitempty"`
}
type LoadBalancerActionUpdateServiceResponse struct {
Action Action `json:"action"`
}
type LoadBalancerDeleteServiceRequest struct {
ListenPort int `json:"listen_port"`
}
type LoadBalancerDeleteServiceResponse struct {
Action Action `json:"action"`
}
type LoadBalancerCreateRequest struct {
Name string `json:"name"`
LoadBalancerType interface{} `json:"load_balancer_type"` // int or string
Algorithm *LoadBalancerCreateRequestAlgorithm `json:"algorithm,omitempty"`
Location *string `json:"location,omitempty"`
NetworkZone *string `json:"network_zone,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
Targets []LoadBalancerCreateRequestTarget `json:"targets,omitempty"`
Services []LoadBalancerCreateRequestService `json:"services,omitempty"`
PublicInterface *bool `json:"public_interface,omitempty"`
Network *int `json:"network,omitempty"`
}
type LoadBalancerCreateRequestAlgorithm struct {
Type string `json:"type"`
}
type LoadBalancerCreateRequestTarget struct {
Type string `json:"type"`
Server *LoadBalancerCreateRequestTargetServer `json:"server,omitempty"`
LabelSelector *LoadBalancerCreateRequestTargetLabelSelector `json:"label_selector,omitempty"`
IP *LoadBalancerCreateRequestTargetIP `json:"ip,omitempty"`
UsePrivateIP *bool `json:"use_private_ip,omitempty"`
}
type LoadBalancerCreateRequestTargetServer struct {
ID int `json:"id"`
}
type LoadBalancerCreateRequestTargetLabelSelector struct {
Selector string `json:"selector"`
}
type LoadBalancerCreateRequestTargetIP struct {
IP string `json:"ip"`
}
type LoadBalancerCreateRequestService struct {
Protocol string `json:"protocol"`
ListenPort *int `json:"listen_port,omitempty"`
DestinationPort *int `json:"destination_port,omitempty"`
Proxyprotocol *bool `json:"proxyprotocol,omitempty"`
HTTP *LoadBalancerCreateRequestServiceHTTP `json:"http,omitempty"`
HealthCheck *LoadBalancerCreateRequestServiceHealthCheck `json:"health_check,omitempty"`
}
type LoadBalancerCreateRequestServiceHTTP struct {
CookieName *string `json:"cookie_name,omitempty"`
CookieLifetime *int `json:"cookie_lifetime,omitempty"`
Certificates *[]int `json:"certificates,omitempty"`
RedirectHTTP *bool `json:"redirect_http,omitempty"`
StickySessions *bool `json:"sticky_sessions,omitempty"`
}
type LoadBalancerCreateRequestServiceHealthCheck struct {
Protocol string `json:"protocol"`
Port *int `json:"port,omitempty"`
Interval *int `json:"interval,omitempty"`
Timeout *int `json:"timeout,omitempty"`
Retries *int `json:"retries,omitempty"`
HTTP *LoadBalancerCreateRequestServiceHealthCheckHTTP `json:"http,omitempty"`
}
type LoadBalancerCreateRequestServiceHealthCheckHTTP struct {
Domain *string `json:"domain,omitempty"`
Path *string `json:"path,omitempty"`
Response *string `json:"response,omitempty"`
StatusCodes *[]string `json:"status_codes,omitempty"`
TLS *bool `json:"tls,omitempty"`
}
type LoadBalancerCreateResponse struct {
LoadBalancer LoadBalancer `json:"load_balancer"`
Action Action `json:"action"`
}
type LoadBalancerActionChangeProtectionRequest struct {
Delete *bool `json:"delete,omitempty"`
}
type LoadBalancerActionChangeProtectionResponse struct {
Action Action `json:"action"`
}
type LoadBalancerUpdateRequest struct {
Name *string `json:"name,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
type LoadBalancerUpdateResponse struct {
LoadBalancer LoadBalancer `json:"load_balancer"`
}
type LoadBalancerActionChangeAlgorithmRequest struct {
Type string `json:"type"`
}
type LoadBalancerActionChangeAlgorithmResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionAttachToNetworkRequest struct {
Network int `json:"network"`
IP *string `json:"ip,omitempty"`
}
type LoadBalancerActionAttachToNetworkResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionDetachFromNetworkRequest struct {
Network int `json:"network"`
}
type LoadBalancerActionDetachFromNetworkResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionEnablePublicInterfaceRequest struct{}
type LoadBalancerActionEnablePublicInterfaceResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionDisablePublicInterfaceRequest struct{}
type LoadBalancerActionDisablePublicInterfaceResponse struct {
Action Action `json:"action"`
}
type LoadBalancerActionChangeTypeRequest struct {
LoadBalancerType interface{} `json:"load_balancer_type"` // int or string
}
type LoadBalancerActionChangeTypeResponse struct {
Action Action `json:"action"`
}

View file

@ -0,0 +1,25 @@
package schema
// LoadBalancerType defines the schema of a LoadBalancer type.
type LoadBalancerType struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
MaxConnections int `json:"max_connections"`
MaxServices int `json:"max_services"`
MaxTargets int `json:"max_targets"`
MaxAssignedCertificates int `json:"max_assigned_certificates"`
Prices []PricingLoadBalancerTypePrice `json:"prices"`
}
// LoadBalancerTypeListResponse defines the schema of the response when
// listing LoadBalancer types.
type LoadBalancerTypeListResponse struct {
LoadBalancerTypes []LoadBalancerType `json:"load_balancer_types"`
}
// LoadBalancerTypeGetResponse defines the schema of the response when
// retrieving a single LoadBalancer type.
type LoadBalancerTypeGetResponse struct {
LoadBalancerType LoadBalancerType `json:"load_balancer_type"`
}

View file

@ -0,0 +1,23 @@
package schema
// Location defines the schema of a location.
type Location struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Country string `json:"country"`
City string `json:"city"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
NetworkZone string `json:"network_zone"`
}
// LocationGetResponse defines the schema of the response when retrieving a single location.
type LocationGetResponse struct {
Location Location `json:"location"`
}
// LocationListResponse defines the schema of the response when listing locations.
type LocationListResponse struct {
Locations []Location `json:"locations"`
}

View file

@ -0,0 +1,23 @@
package schema
// Meta defines the schema of meta information which may be included
// in responses.
type Meta struct {
Pagination *MetaPagination `json:"pagination"`
}
// MetaPagination defines the schema of pagination information.
type MetaPagination struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
PreviousPage int `json:"previous_page"`
NextPage int `json:"next_page"`
LastPage int `json:"last_page"`
TotalEntries int `json:"total_entries"`
}
// MetaResponse defines the schema of a response containing
// meta information.
type MetaResponse struct {
Meta Meta `json:"meta"`
}

View file

@ -0,0 +1,150 @@
package schema
import "time"
// Network defines the schema of a network.
type Network struct {
ID int `json:"id"`
Name string `json:"name"`
Created time.Time `json:"created"`
IPRange string `json:"ip_range"`
Subnets []NetworkSubnet `json:"subnets"`
Routes []NetworkRoute `json:"routes"`
Servers []int `json:"servers"`
Protection NetworkProtection `json:"protection"`
Labels map[string]string `json:"labels"`
}
// NetworkSubnet represents a subnet of a network.
type NetworkSubnet struct {
Type string `json:"type"`
IPRange string `json:"ip_range"`
NetworkZone string `json:"network_zone"`
Gateway string `json:"gateway"`
}
// NetworkRoute represents a route of a network.
type NetworkRoute struct {
Destination string `json:"destination"`
Gateway string `json:"gateway"`
}
// NetworkProtection represents the protection level of a network.
type NetworkProtection struct {
Delete bool `json:"delete"`
}
// NetworkUpdateRequest defines the schema of the request to update a network.
type NetworkUpdateRequest struct {
Name string `json:"name,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
// NetworkUpdateResponse defines the schema of the response when updating a network.
type NetworkUpdateResponse struct {
Network Network `json:"network"`
}
// NetworkListResponse defines the schema of the response when
// listing networks.
type NetworkListResponse struct {
Networks []Network `json:"networks"`
}
// NetworkGetResponse defines the schema of the response when
// retrieving a single network.
type NetworkGetResponse struct {
Network Network `json:"network"`
}
// NetworkCreateRequest defines the schema of the request to create a network.
type NetworkCreateRequest struct {
Name string `json:"name"`
IPRange string `json:"ip_range"`
Subnets []NetworkSubnet `json:"subnets,omitempty"`
Routes []NetworkRoute `json:"routes,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
// NetworkCreateResponse defines the schema of the response when
// creating a network.
type NetworkCreateResponse struct {
Network Network `json:"network"`
}
// NetworkActionChangeIPRangeRequest defines the schema of the request to
// change the IP range of a network.
type NetworkActionChangeIPRangeRequest struct {
IPRange string `json:"ip_range"`
}
// NetworkActionChangeIPRangeResponse defines the schema of the response when
// changing the IP range of a network.
type NetworkActionChangeIPRangeResponse struct {
Action Action `json:"action"`
}
// NetworkActionAddSubnetRequest defines the schema of the request to
// add a subnet to a network.
type NetworkActionAddSubnetRequest struct {
Type string `json:"type"`
IPRange string `json:"ip_range,omitempty"`
NetworkZone string `json:"network_zone"`
Gateway string `json:"gateway"`
}
// NetworkActionAddSubnetResponse defines the schema of the response when
// adding a subnet to a network.
type NetworkActionAddSubnetResponse struct {
Action Action `json:"action"`
}
// NetworkActionDeleteSubnetRequest defines the schema of the request to
// delete a subnet from a network.
type NetworkActionDeleteSubnetRequest struct {
IPRange string `json:"ip_range"`
}
// NetworkActionDeleteSubnetResponse defines the schema of the response when
// deleting a subnet from a network.
type NetworkActionDeleteSubnetResponse struct {
Action Action `json:"action"`
}
// NetworkActionAddRouteRequest defines the schema of the request to
// add a route to a network.
type NetworkActionAddRouteRequest struct {
Destination string `json:"destination"`
Gateway string `json:"gateway"`
}
// NetworkActionAddRouteResponse defines the schema of the response when
// adding a route to a network.
type NetworkActionAddRouteResponse struct {
Action Action `json:"action"`
}
// NetworkActionDeleteRouteRequest defines the schema of the request to
// delete a route from a network.
type NetworkActionDeleteRouteRequest struct {
Destination string `json:"destination"`
Gateway string `json:"gateway"`
}
// NetworkActionDeleteRouteResponse defines the schema of the response when
// deleting a route from a network.
type NetworkActionDeleteRouteResponse struct {
Action Action `json:"action"`
}
// NetworkActionChangeProtectionRequest defines the schema of the request to
// change the resource protection of a network.
type NetworkActionChangeProtectionRequest struct {
Delete *bool `json:"delete,omitempty"`
}
// NetworkActionChangeProtectionResponse defines the schema of the response when
// changing the resource protection of a network.
type NetworkActionChangeProtectionResponse struct {
Action Action `json:"action"`
}

View file

@ -0,0 +1,74 @@
package schema
// Pricing defines the schema for pricing information.
type Pricing struct {
Currency string `json:"currency"`
VATRate string `json:"vat_rate"`
Image PricingImage `json:"image"`
FloatingIP PricingFloatingIP `json:"floating_ip"`
Traffic PricingTraffic `json:"traffic"`
ServerBackup PricingServerBackup `json:"server_backup"`
ServerTypes []PricingServerType `json:"server_types"`
LoadBalancerTypes []PricingLoadBalancerType `json:"load_balancer_types"`
}
// Price defines the schema of a single price with net and gross amount.
type Price struct {
Net string `json:"net"`
Gross string `json:"gross"`
}
// PricingImage defines the schema of pricing information for an image.
type PricingImage struct {
PricePerGBMonth Price `json:"price_per_gb_month"`
}
// PricingFloatingIP defines the schema of pricing information for a Floating IP.
type PricingFloatingIP struct {
PriceMonthly Price `json:"price_monthly"`
}
// PricingTraffic defines the schema of pricing information for traffic.
type PricingTraffic struct {
PricePerTB Price `json:"price_per_tb"`
}
// PricingServerBackup defines the schema of pricing information for server backups.
type PricingServerBackup struct {
Percentage string `json:"percentage"`
}
// PricingServerType defines the schema of pricing information for a server type.
type PricingServerType struct {
ID int `json:"id"`
Name string `json:"name"`
Prices []PricingServerTypePrice `json:"prices"`
}
// PricingServerTypePrice defines the schema of pricing information for a server
// type at a location.
type PricingServerTypePrice struct {
Location string `json:"location"`
PriceHourly Price `json:"price_hourly"`
PriceMonthly Price `json:"price_monthly"`
}
// PricingLoadBalancerType defines the schema of pricing information for a Load Balancer type.
type PricingLoadBalancerType struct {
ID int `json:"id"`
Name string `json:"name"`
Prices []PricingLoadBalancerTypePrice `json:"prices"`
}
// PricingLoadBalancerTypePrice defines the schema of pricing information for a Load Balancer
// type at a location.
type PricingLoadBalancerTypePrice struct {
Location string `json:"location"`
PriceHourly Price `json:"price_hourly"`
PriceMonthly Price `json:"price_monthly"`
}
// PricingGetResponse defines the schema of the response when retrieving pricing information.
type PricingGetResponse struct {
Pricing Pricing `json:"pricing"`
}

View file

@ -0,0 +1,366 @@
package schema
import "time"
// Server defines the schema of a server.
type Server struct {
ID int `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Created time.Time `json:"created"`
PublicNet ServerPublicNet `json:"public_net"`
PrivateNet []ServerPrivateNet `json:"private_net"`
ServerType ServerType `json:"server_type"`
IncludedTraffic uint64 `json:"included_traffic"`
OutgoingTraffic *uint64 `json:"outgoing_traffic"`
IngoingTraffic *uint64 `json:"ingoing_traffic"`
BackupWindow *string `json:"backup_window"`
RescueEnabled bool `json:"rescue_enabled"`
ISO *ISO `json:"iso"`
Locked bool `json:"locked"`
Datacenter Datacenter `json:"datacenter"`
Image *Image `json:"image"`
Protection ServerProtection `json:"protection"`
Labels map[string]string `json:"labels"`
Volumes []int `json:"volumes"`
}
// ServerProtection defines the schema of a server's resource protection.
type ServerProtection struct {
Delete bool `json:"delete"`
Rebuild bool `json:"rebuild"`
}
// ServerPublicNet defines the schema of a server's
// public network information.
type ServerPublicNet struct {
IPv4 ServerPublicNetIPv4 `json:"ipv4"`
IPv6 ServerPublicNetIPv6 `json:"ipv6"`
FloatingIPs []int `json:"floating_ips"`
}
// ServerPublicNetIPv4 defines the schema of a server's public
// network information for an IPv4.
type ServerPublicNetIPv4 struct {
IP string `json:"ip"`
Blocked bool `json:"blocked"`
DNSPtr string `json:"dns_ptr"`
}
// ServerPublicNetIPv6 defines the schema of a server's public
// network information for an IPv6.
type ServerPublicNetIPv6 struct {
IP string `json:"ip"`
Blocked bool `json:"blocked"`
DNSPtr []ServerPublicNetIPv6DNSPtr `json:"dns_ptr"`
}
// ServerPublicNetIPv6DNSPtr defines the schema of a server's
// public network information for an IPv6 reverse DNS.
type ServerPublicNetIPv6DNSPtr struct {
IP string `json:"ip"`
DNSPtr string `json:"dns_ptr"`
}
// ServerPrivateNet defines the schema of a server's private network information.
type ServerPrivateNet struct {
Network int `json:"network"`
IP string `json:"ip"`
AliasIPs []string `json:"alias_ips"`
MACAddress string `json:"mac_address"`
}
// ServerGetResponse defines the schema of the response when
// retrieving a single server.
type ServerGetResponse struct {
Server Server `json:"server"`
}
// ServerListResponse defines the schema of the response when
// listing servers.
type ServerListResponse struct {
Servers []Server `json:"servers"`
}
// ServerCreateRequest defines the schema for the request to
// create a server.
type ServerCreateRequest struct {
Name string `json:"name"`
ServerType interface{} `json:"server_type"` // int or string
Image interface{} `json:"image"` // int or string
SSHKeys []int `json:"ssh_keys,omitempty"`
Location string `json:"location,omitempty"`
Datacenter string `json:"datacenter,omitempty"`
UserData string `json:"user_data,omitempty"`
StartAfterCreate *bool `json:"start_after_create,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
Automount *bool `json:"automount,omitempty"`
Volumes []int `json:"volumes,omitempty"`
Networks []int `json:"networks,omitempty"`
}
// ServerCreateResponse defines the schema of the response when
// creating a server.
type ServerCreateResponse struct {
Server Server `json:"server"`
Action Action `json:"action"`
RootPassword *string `json:"root_password"`
NextActions []Action `json:"next_actions"`
}
// ServerUpdateRequest defines the schema of the request to update a server.
type ServerUpdateRequest struct {
Name string `json:"name,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
// ServerUpdateResponse defines the schema of the response when updating a server.
type ServerUpdateResponse struct {
Server Server `json:"server"`
}
// ServerActionPoweronRequest defines the schema for the request to
// create a poweron server action.
type ServerActionPoweronRequest struct{}
// ServerActionPoweronResponse defines the schema of the response when
// creating a poweron server action.
type ServerActionPoweronResponse struct {
Action Action `json:"action"`
}
// ServerActionPoweroffRequest defines the schema for the request to
// create a poweroff server action.
type ServerActionPoweroffRequest struct{}
// ServerActionPoweroffResponse defines the schema of the response when
// creating a poweroff server action.
type ServerActionPoweroffResponse struct {
Action Action `json:"action"`
}
// ServerActionRebootRequest defines the schema for the request to
// create a reboot server action.
type ServerActionRebootRequest struct{}
// ServerActionRebootResponse defines the schema of the response when
// creating a reboot server action.
type ServerActionRebootResponse struct {
Action Action `json:"action"`
}
// ServerActionResetRequest defines the schema for the request to
// create a reset server action.
type ServerActionResetRequest struct{}
// ServerActionResetResponse defines the schema of the response when
// creating a reset server action.
type ServerActionResetResponse struct {
Action Action `json:"action"`
}
// ServerActionShutdownRequest defines the schema for the request to
// create a shutdown server action.
type ServerActionShutdownRequest struct{}
// ServerActionShutdownResponse defines the schema of the response when
// creating a shutdown server action.
type ServerActionShutdownResponse struct {
Action Action `json:"action"`
}
// ServerActionResetPasswordRequest defines the schema for the request to
// create a reset_password server action.
type ServerActionResetPasswordRequest struct{}
// ServerActionResetPasswordResponse defines the schema of the response when
// creating a reset_password server action.
type ServerActionResetPasswordResponse struct {
Action Action `json:"action"`
RootPassword string `json:"root_password"`
}
// ServerActionCreateImageRequest defines the schema for the request to
// create a create_image server action.
type ServerActionCreateImageRequest struct {
Type *string `json:"type"`
Description *string `json:"description"`
Labels *map[string]string `json:"labels,omitempty"`
}
// ServerActionCreateImageResponse defines the schema of the response when
// creating a create_image server action.
type ServerActionCreateImageResponse struct {
Action Action `json:"action"`
Image Image `json:"image"`
}
// ServerActionEnableRescueRequest defines the schema for the request to
// create a enable_rescue server action.
type ServerActionEnableRescueRequest struct {
Type *string `json:"type,omitempty"`
SSHKeys []int `json:"ssh_keys,omitempty"`
}
// ServerActionEnableRescueResponse defines the schema of the response when
// creating a enable_rescue server action.
type ServerActionEnableRescueResponse struct {
Action Action `json:"action"`
RootPassword string `json:"root_password"`
}
// ServerActionDisableRescueRequest defines the schema for the request to
// create a disable_rescue server action.
type ServerActionDisableRescueRequest struct{}
// ServerActionDisableRescueResponse defines the schema of the response when
// creating a disable_rescue server action.
type ServerActionDisableRescueResponse struct {
Action Action `json:"action"`
}
// ServerActionRebuildRequest defines the schema for the request to
// rebuild a server.
type ServerActionRebuildRequest struct {
Image interface{} `json:"image"` // int or string
}
// ServerActionRebuildResponse defines the schema of the response when
// creating a rebuild server action.
type ServerActionRebuildResponse struct {
Action Action `json:"action"`
}
// ServerActionAttachISORequest defines the schema for the request to
// attach an ISO to a server.
type ServerActionAttachISORequest struct {
ISO interface{} `json:"iso"` // int or string
}
// ServerActionAttachISOResponse defines the schema of the response when
// creating a attach_iso server action.
type ServerActionAttachISOResponse struct {
Action Action `json:"action"`
}
// ServerActionDetachISORequest defines the schema for the request to
// detach an ISO from a server.
type ServerActionDetachISORequest struct{}
// ServerActionDetachISOResponse defines the schema of the response when
// creating a detach_iso server action.
type ServerActionDetachISOResponse struct {
Action Action `json:"action"`
}
// ServerActionEnableBackupRequest defines the schema for the request to
// enable backup for a server.
type ServerActionEnableBackupRequest struct {
BackupWindow *string `json:"backup_window,omitempty"`
}
// ServerActionEnableBackupResponse defines the schema of the response when
// creating a enable_backup server action.
type ServerActionEnableBackupResponse struct {
Action Action `json:"action"`
}
// ServerActionDisableBackupRequest defines the schema for the request to
// disable backup for a server.
type ServerActionDisableBackupRequest struct{}
// ServerActionDisableBackupResponse defines the schema of the response when
// creating a disable_backup server action.
type ServerActionDisableBackupResponse struct {
Action Action `json:"action"`
}
// ServerActionChangeTypeRequest defines the schema for the request to
// change a server's type.
type ServerActionChangeTypeRequest struct {
ServerType interface{} `json:"server_type"` // int or string
UpgradeDisk bool `json:"upgrade_disk"`
}
// ServerActionChangeTypeResponse defines the schema of the response when
// creating a change_type server action.
type ServerActionChangeTypeResponse struct {
Action Action `json:"action"`
}
// ServerActionChangeDNSPtrRequest defines the schema for the request to
// change a server's reverse DNS pointer.
type ServerActionChangeDNSPtrRequest struct {
IP string `json:"ip"`
DNSPtr *string `json:"dns_ptr"`
}
// ServerActionChangeDNSPtrResponse defines the schema of the response when
// creating a change_dns_ptr server action.
type ServerActionChangeDNSPtrResponse struct {
Action Action `json:"action"`
}
// ServerActionChangeProtectionRequest defines the schema of the request to
// change the resource protection of a server.
type ServerActionChangeProtectionRequest struct {
Rebuild *bool `json:"rebuild,omitempty"`
Delete *bool `json:"delete,omitempty"`
}
// ServerActionChangeProtectionResponse defines the schema of the response when
// changing the resource protection of a server.
type ServerActionChangeProtectionResponse struct {
Action Action `json:"action"`
}
// ServerActionRequestConsoleRequest defines the schema of the request to
// request a WebSocket VNC console.
type ServerActionRequestConsoleRequest struct{}
// ServerActionRequestConsoleResponse defines the schema of the response when
// requesting a WebSocket VNC console.
type ServerActionRequestConsoleResponse struct {
Action Action `json:"action"`
WSSURL string `json:"wss_url"`
Password string `json:"password"`
}
// ServerActionAttachToNetworkRequest defines the schema for the request to
// attach a network to a server.
type ServerActionAttachToNetworkRequest struct {
Network int `json:"network"`
IP *string `json:"ip,omitempty"`
AliasIPs []*string `json:"alias_ips,omitempty"`
}
// ServerActionAttachToNetworkResponse defines the schema of the response when
// creating an attach_to_network server action.
type ServerActionAttachToNetworkResponse struct {
Action Action `json:"action"`
}
// ServerActionDetachFromNetworkRequest defines the schema for the request to
// detach a network from a server.
type ServerActionDetachFromNetworkRequest struct {
Network int `json:"network"`
}
// ServerActionDetachFromNetworkResponse defines the schema of the response when
// creating a detach_from_network server action.
type ServerActionDetachFromNetworkResponse struct {
Action Action `json:"action"`
}
// ServerActionChangeAliasIPsRequest defines the schema for the request to
// change a server's alias IPs in a network.
type ServerActionChangeAliasIPsRequest struct {
Network int `json:"network"`
AliasIPs []string `json:"alias_ips"`
}
// ServerActionChangeAliasIPsResponse defines the schema of the response when
// creating an change_alias_ips server action.
type ServerActionChangeAliasIPsResponse struct {
Action Action `json:"action"`
}

View file

@ -0,0 +1,26 @@
package schema
// ServerType defines the schema of a server type.
type ServerType struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Cores int `json:"cores"`
Memory float32 `json:"memory"`
Disk int `json:"disk"`
StorageType string `json:"storage_type"`
CPUType string `json:"cpu_type"`
Prices []PricingServerTypePrice `json:"prices"`
}
// ServerTypeListResponse defines the schema of the response when
// listing server types.
type ServerTypeListResponse struct {
ServerTypes []ServerType `json:"server_types"`
}
// ServerTypeGetResponse defines the schema of the response when
// retrieving a single server type.
type ServerTypeGetResponse struct {
ServerType ServerType `json:"server_type"`
}

View file

@ -0,0 +1,50 @@
package schema
import "time"
// SSHKey defines the schema of a SSH key.
type SSHKey struct {
ID int `json:"id"`
Name string `json:"name"`
Fingerprint string `json:"fingerprint"`
PublicKey string `json:"public_key"`
Labels map[string]string `json:"labels"`
Created time.Time `json:"created"`
}
// SSHKeyCreateRequest defines the schema of the request
// to create a SSH key.
type SSHKeyCreateRequest struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
Labels *map[string]string `json:"labels,omitempty"`
}
// SSHKeyCreateResponse defines the schema of the response
// when creating a SSH key.
type SSHKeyCreateResponse struct {
SSHKey SSHKey `json:"ssh_key"`
}
// SSHKeyListResponse defines the schema of the response
// when listing SSH keys.
type SSHKeyListResponse struct {
SSHKeys []SSHKey `json:"ssh_keys"`
}
// SSHKeyGetResponse defines the schema of the response
// when retrieving a single SSH key.
type SSHKeyGetResponse struct {
SSHKey SSHKey `json:"ssh_key"`
}
// SSHKeyUpdateRequest defines the schema of the request to update a SSH key.
type SSHKeyUpdateRequest struct {
Name string `json:"name,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
// SSHKeyUpdateResponse defines the schema of the response when updating a SSH key.
type SSHKeyUpdateResponse struct {
SSHKey SSHKey `json:"ssh_key"`
}

View file

@ -0,0 +1,110 @@
package schema
import "time"
// Volume defines the schema of a volume.
type Volume struct {
ID int `json:"id"`
Name string `json:"name"`
Server *int `json:"server"`
Status string `json:"status"`
Location Location `json:"location"`
Size int `json:"size"`
Protection VolumeProtection `json:"protection"`
Labels map[string]string `json:"labels"`
LinuxDevice string `json:"linux_device"`
Created time.Time `json:"created"`
}
// VolumeCreateRequest defines the schema of the request
// to create a volume.
type VolumeCreateRequest struct {
Name string `json:"name"`
Size int `json:"size"`
Server *int `json:"server,omitempty"`
Location interface{} `json:"location,omitempty"` // int, string, or nil
Labels *map[string]string `json:"labels,omitempty"`
Automount *bool `json:"automount,omitempty"`
Format *string `json:"format,omitempty"`
}
// VolumeCreateResponse defines the schema of the response
// when creating a volume.
type VolumeCreateResponse struct {
Volume Volume `json:"volume"`
Action *Action `json:"action"`
NextActions []Action `json:"next_actions"`
}
// VolumeListResponse defines the schema of the response
// when listing volumes.
type VolumeListResponse struct {
Volumes []Volume `json:"volumes"`
}
// VolumeGetResponse defines the schema of the response
// when retrieving a single volume.
type VolumeGetResponse struct {
Volume Volume `json:"volume"`
}
// VolumeUpdateRequest defines the schema of the request to update a volume.
type VolumeUpdateRequest struct {
Name string `json:"name,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
// VolumeUpdateResponse defines the schema of the response when updating a volume.
type VolumeUpdateResponse struct {
Volume Volume `json:"volume"`
}
// VolumeProtection defines the schema of a volume's resource protection.
type VolumeProtection struct {
Delete bool `json:"delete"`
}
// VolumeActionChangeProtectionRequest defines the schema of the request to
// change the resource protection of a volume.
type VolumeActionChangeProtectionRequest struct {
Delete *bool `json:"delete,omitempty"`
}
// VolumeActionChangeProtectionResponse defines the schema of the response when
// changing the resource protection of a volume.
type VolumeActionChangeProtectionResponse struct {
Action Action `json:"action"`
}
// VolumeActionAttachVolumeRequest defines the schema of the request to
// attach a volume to a server.
type VolumeActionAttachVolumeRequest struct {
Server int `json:"server"`
Automount *bool `json:"automount,omitempty"`
}
// VolumeActionAttachVolumeResponse defines the schema of the response when
// attaching a volume to a server.
type VolumeActionAttachVolumeResponse struct {
Action Action `json:"action"`
}
// VolumeActionDetachVolumeRequest defines the schema of the request to
// create an detach volume action.
type VolumeActionDetachVolumeRequest struct{}
// VolumeActionDetachVolumeResponse defines the schema of the response when
// creating an detach volume action.
type VolumeActionDetachVolumeResponse struct {
Action Action `json:"action"`
}
// VolumeActionResizeVolumeRequest defines the schema of the request to resize a volume.
type VolumeActionResizeVolumeRequest struct {
Size int `json:"size"`
}
// VolumeActionResizeVolumeResponse defines the schema of the response when resizing a volume.
type VolumeActionResizeVolumeResponse struct {
Action Action `json:"action"`
}

View file

@ -0,0 +1,953 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Server represents a server in the Hetzner Cloud.
type Server struct {
ID int
Name string
Status ServerStatus
Created time.Time
PublicNet ServerPublicNet
PrivateNet []ServerPrivateNet
ServerType *ServerType
Datacenter *Datacenter
IncludedTraffic uint64
OutgoingTraffic uint64
IngoingTraffic uint64
BackupWindow string
RescueEnabled bool
Locked bool
ISO *ISO
Image *Image
Protection ServerProtection
Labels map[string]string
Volumes []*Volume
}
// ServerProtection represents the protection level of a server.
type ServerProtection struct {
Delete, Rebuild bool
}
// ServerStatus specifies a server's status.
type ServerStatus string
const (
// ServerStatusInitializing is the status when a server is initializing.
ServerStatusInitializing ServerStatus = "initializing"
// ServerStatusOff is the status when a server is off.
ServerStatusOff ServerStatus = "off"
// ServerStatusRunning is the status when a server is running.
ServerStatusRunning ServerStatus = "running"
// ServerStatusStarting is the status when a server is being started.
ServerStatusStarting ServerStatus = "starting"
// ServerStatusStopping is the status when a server is being stopped.
ServerStatusStopping ServerStatus = "stopping"
// ServerStatusMigrating is the status when a server is being migrated.
ServerStatusMigrating ServerStatus = "migrating"
// ServerStatusRebuilding is the status when a server is being rebuilt.
ServerStatusRebuilding ServerStatus = "rebuilding"
// ServerStatusDeleting is the status when a server is being deleted.
ServerStatusDeleting ServerStatus = "deleting"
// ServerStatusUnknown is the status when a server's state is unknown.
ServerStatusUnknown ServerStatus = "unknown"
)
// ServerPublicNet represents a server's public network.
type ServerPublicNet struct {
IPv4 ServerPublicNetIPv4
IPv6 ServerPublicNetIPv6
FloatingIPs []*FloatingIP
}
// ServerPublicNetIPv4 represents a server's public IPv4 address.
type ServerPublicNetIPv4 struct {
IP net.IP
Blocked bool
DNSPtr string
}
// ServerPublicNetIPv6 represents a server's public IPv6 network and address.
type ServerPublicNetIPv6 struct {
IP net.IP
Network *net.IPNet
Blocked bool
DNSPtr map[string]string
}
// ServerPrivateNet defines the schema of a server's private network information.
type ServerPrivateNet struct {
Network *Network
IP net.IP
Aliases []net.IP
MACAddress string
}
// DNSPtrForIP returns the reverse dns pointer of the ip address.
func (s *ServerPublicNetIPv6) DNSPtrForIP(ip net.IP) string {
return s.DNSPtr[ip.String()]
}
// ServerRescueType represents rescue types.
type ServerRescueType string
// List of rescue types.
const (
ServerRescueTypeLinux32 ServerRescueType = "linux32"
ServerRescueTypeLinux64 ServerRescueType = "linux64"
ServerRescueTypeFreeBSD64 ServerRescueType = "freebsd64"
)
// ServerClient is a client for the servers API.
type ServerClient struct {
client *Client
}
// GetByID retrieves a server by its ID. If the server does not exist, nil is returned.
func (c *ServerClient) GetByID(ctx context.Context, id int) (*Server, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/servers/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.ServerGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return ServerFromSchema(body.Server), resp, nil
}
// GetByName retrieves a server by its name. If the server does not exist, nil is returned.
func (c *ServerClient) GetByName(ctx context.Context, name string) (*Server, *Response, error) {
if name == "" {
return nil, nil, nil
}
servers, response, err := c.List(ctx, ServerListOpts{Name: name})
if len(servers) == 0 {
return nil, response, err
}
return servers[0], response, err
}
// Get retrieves a server by its ID if the input can be parsed as an integer, otherwise it
// retrieves a server by its name. If the server does not exist, nil is returned.
func (c *ServerClient) Get(ctx context.Context, idOrName string) (*Server, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// ServerListOpts specifies options for listing servers.
type ServerListOpts struct {
ListOpts
Name string
Status []ServerStatus
}
func (l ServerListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
for _, status := range l.Status {
vals.Add("status", string(status))
}
return vals
}
// List returns a list of servers for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *ServerClient) List(ctx context.Context, opts ServerListOpts) ([]*Server, *Response, error) {
path := "/servers?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.ServerListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
servers := make([]*Server, 0, len(body.Servers))
for _, s := range body.Servers {
servers = append(servers, ServerFromSchema(s))
}
return servers, resp, nil
}
// All returns all servers.
func (c *ServerClient) All(ctx context.Context) ([]*Server, error) {
return c.AllWithOpts(ctx, ServerListOpts{ListOpts: ListOpts{PerPage: 50}})
}
// AllWithOpts returns all servers for the given options.
func (c *ServerClient) AllWithOpts(ctx context.Context, opts ServerListOpts) ([]*Server, error) {
allServers := []*Server{}
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
servers, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allServers = append(allServers, servers...)
return resp, nil
})
if err != nil {
return nil, err
}
return allServers, nil
}
// ServerCreateOpts specifies options for creating a new server.
type ServerCreateOpts struct {
Name string
ServerType *ServerType
Image *Image
SSHKeys []*SSHKey
Location *Location
Datacenter *Datacenter
UserData string
StartAfterCreate *bool
Labels map[string]string
Automount *bool
Volumes []*Volume
Networks []*Network
}
// Validate checks if options are valid.
func (o ServerCreateOpts) Validate() error {
if o.Name == "" {
return errors.New("missing name")
}
if o.ServerType == nil || (o.ServerType.ID == 0 && o.ServerType.Name == "") {
return errors.New("missing server type")
}
if o.Image == nil || (o.Image.ID == 0 && o.Image.Name == "") {
return errors.New("missing image")
}
if o.Location != nil && o.Datacenter != nil {
return errors.New("location and datacenter are mutually exclusive")
}
return nil
}
// ServerCreateResult is the result of a create server call.
type ServerCreateResult struct {
Server *Server
Action *Action
RootPassword string
NextActions []*Action
}
// Create creates a new server.
func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (ServerCreateResult, *Response, error) {
if err := opts.Validate(); err != nil {
return ServerCreateResult{}, nil, err
}
var reqBody schema.ServerCreateRequest
reqBody.UserData = opts.UserData
reqBody.Name = opts.Name
reqBody.Automount = opts.Automount
reqBody.StartAfterCreate = opts.StartAfterCreate
if opts.ServerType.ID != 0 {
reqBody.ServerType = opts.ServerType.ID
} else if opts.ServerType.Name != "" {
reqBody.ServerType = opts.ServerType.Name
}
if opts.Image.ID != 0 {
reqBody.Image = opts.Image.ID
} else if opts.Image.Name != "" {
reqBody.Image = opts.Image.Name
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
for _, sshKey := range opts.SSHKeys {
reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID)
}
for _, volume := range opts.Volumes {
reqBody.Volumes = append(reqBody.Volumes, volume.ID)
}
for _, network := range opts.Networks {
reqBody.Networks = append(reqBody.Networks, network.ID)
}
if opts.Location != nil {
if opts.Location.ID != 0 {
reqBody.Location = strconv.Itoa(opts.Location.ID)
} else {
reqBody.Location = opts.Location.Name
}
}
if opts.Datacenter != nil {
if opts.Datacenter.ID != 0 {
reqBody.Datacenter = strconv.Itoa(opts.Datacenter.ID)
} else {
reqBody.Datacenter = opts.Datacenter.Name
}
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return ServerCreateResult{}, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/servers", bytes.NewReader(reqBodyData))
if err != nil {
return ServerCreateResult{}, nil, err
}
var respBody schema.ServerCreateResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return ServerCreateResult{}, resp, err
}
result := ServerCreateResult{
Server: ServerFromSchema(respBody.Server),
Action: ActionFromSchema(respBody.Action),
NextActions: ActionsFromSchema(respBody.NextActions),
}
if respBody.RootPassword != nil {
result.RootPassword = *respBody.RootPassword
}
return result, resp, nil
}
// Delete deletes a server.
func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/servers/%d", server.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}
// ServerUpdateOpts specifies options for updating a server.
type ServerUpdateOpts struct {
Name string
Labels map[string]string
}
// Update updates a server.
func (c *ServerClient) Update(ctx context.Context, server *Server, opts ServerUpdateOpts) (*Server, *Response, error) {
reqBody := schema.ServerUpdateRequest{
Name: opts.Name,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d", server.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ServerFromSchema(respBody.Server), resp, nil
}
// Poweron starts a server.
func (c *ServerClient) Poweron(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/poweron", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionPoweronResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// Reboot reboots a server.
func (c *ServerClient) Reboot(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/reboot", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionRebootResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// Reset resets a server.
func (c *ServerClient) Reset(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/reset", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionResetResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// Shutdown shuts down a server.
func (c *ServerClient) Shutdown(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/shutdown", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionShutdownResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// Poweroff stops a server.
func (c *ServerClient) Poweroff(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/poweroff", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionPoweroffResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// ServerResetPasswordResult is the result of resetting a server's password.
type ServerResetPasswordResult struct {
Action *Action
RootPassword string
}
// ResetPassword resets a server's password.
func (c *ServerClient) ResetPassword(ctx context.Context, server *Server) (ServerResetPasswordResult, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/reset_password", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return ServerResetPasswordResult{}, nil, err
}
respBody := schema.ServerActionResetPasswordResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return ServerResetPasswordResult{}, resp, err
}
return ServerResetPasswordResult{
Action: ActionFromSchema(respBody.Action),
RootPassword: respBody.RootPassword,
}, resp, nil
}
// ServerCreateImageOpts specifies options for creating an image from a server.
type ServerCreateImageOpts struct {
Type ImageType
Description *string
Labels map[string]string
}
// Validate checks if options are valid.
func (o ServerCreateImageOpts) Validate() error {
switch o.Type {
case ImageTypeSnapshot, ImageTypeBackup:
break
case "":
break
default:
return errors.New("invalid type")
}
return nil
}
// ServerCreateImageResult is the result of creating an image from a server.
type ServerCreateImageResult struct {
Action *Action
Image *Image
}
// CreateImage creates an image from a server.
func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *ServerCreateImageOpts) (ServerCreateImageResult, *Response, error) {
var reqBody schema.ServerActionCreateImageRequest
if opts != nil {
if err := opts.Validate(); err != nil {
return ServerCreateImageResult{}, nil, fmt.Errorf("invalid options: %s", err)
}
if opts.Description != nil {
reqBody.Description = opts.Description
}
if opts.Type != "" {
reqBody.Type = String(string(opts.Type))
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return ServerCreateImageResult{}, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/create_image", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return ServerCreateImageResult{}, nil, err
}
respBody := schema.ServerActionCreateImageResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return ServerCreateImageResult{}, resp, err
}
return ServerCreateImageResult{
Action: ActionFromSchema(respBody.Action),
Image: ImageFromSchema(respBody.Image),
}, resp, nil
}
// ServerEnableRescueOpts specifies options for enabling rescue mode for a server.
type ServerEnableRescueOpts struct {
Type ServerRescueType
SSHKeys []*SSHKey
}
// ServerEnableRescueResult is the result of enabling rescue mode for a server.
type ServerEnableRescueResult struct {
Action *Action
RootPassword string
}
// EnableRescue enables rescue mode for a server.
func (c *ServerClient) EnableRescue(ctx context.Context, server *Server, opts ServerEnableRescueOpts) (ServerEnableRescueResult, *Response, error) {
reqBody := schema.ServerActionEnableRescueRequest{
Type: String(string(opts.Type)),
}
for _, sshKey := range opts.SSHKeys {
reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID)
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return ServerEnableRescueResult{}, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/enable_rescue", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return ServerEnableRescueResult{}, nil, err
}
respBody := schema.ServerActionEnableRescueResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return ServerEnableRescueResult{}, resp, err
}
result := ServerEnableRescueResult{
Action: ActionFromSchema(respBody.Action),
RootPassword: respBody.RootPassword,
}
return result, resp, nil
}
// DisableRescue disables rescue mode for a server.
func (c *ServerClient) DisableRescue(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/disable_rescue", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionDisableRescueResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// ServerRebuildOpts specifies options for rebuilding a server.
type ServerRebuildOpts struct {
Image *Image
}
// Rebuild rebuilds a server.
func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerRebuildOpts) (*Action, *Response, error) {
reqBody := schema.ServerActionRebuildRequest{}
if opts.Image.ID != 0 {
reqBody.Image = opts.Image.ID
} else {
reqBody.Image = opts.Image.Name
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/rebuild", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionRebuildResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// AttachISO attaches an ISO to a server.
func (c *ServerClient) AttachISO(ctx context.Context, server *Server, iso *ISO) (*Action, *Response, error) {
reqBody := schema.ServerActionAttachISORequest{}
if iso.ID != 0 {
reqBody.ISO = iso.ID
} else {
reqBody.ISO = iso.Name
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/attach_iso", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionAttachISOResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// DetachISO detaches the currently attached ISO from a server.
func (c *ServerClient) DetachISO(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/detach_iso", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionDetachISOResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// EnableBackup enables backup for a server. Pass in an empty backup window to let the
// API pick a window for you. See the API documentation at docs.hetzner.cloud for a list
// of valid backup windows.
func (c *ServerClient) EnableBackup(ctx context.Context, server *Server, window string) (*Action, *Response, error) {
reqBody := schema.ServerActionEnableBackupRequest{}
if window != "" {
reqBody.BackupWindow = String(window)
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/enable_backup", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionEnableBackupResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// DisableBackup disables backup for a server.
func (c *ServerClient) DisableBackup(ctx context.Context, server *Server) (*Action, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/disable_backup", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionDisableBackupResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// ServerChangeTypeOpts specifies options for changing a server's type.
type ServerChangeTypeOpts struct {
ServerType *ServerType // new server type
UpgradeDisk bool // whether disk should be upgraded
}
// ChangeType changes a server's type.
func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts ServerChangeTypeOpts) (*Action, *Response, error) {
reqBody := schema.ServerActionChangeTypeRequest{
UpgradeDisk: opts.UpgradeDisk,
}
if opts.ServerType.ID != 0 {
reqBody.ServerType = opts.ServerType.ID
} else {
reqBody.ServerType = opts.ServerType.Name
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/change_type", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionChangeTypeResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// ChangeDNSPtr changes or resets the reverse DNS pointer for a server IP address.
// Pass a nil ptr to reset the reverse DNS pointer to its default value.
func (c *ServerClient) ChangeDNSPtr(ctx context.Context, server *Server, ip string, ptr *string) (*Action, *Response, error) {
reqBody := schema.ServerActionChangeDNSPtrRequest{
IP: ip,
DNSPtr: ptr,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionChangeDNSPtrResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// ServerChangeProtectionOpts specifies options for changing the resource protection level of a server.
type ServerChangeProtectionOpts struct {
Rebuild *bool
Delete *bool
}
// ChangeProtection changes the resource protection level of a server.
func (c *ServerClient) ChangeProtection(ctx context.Context, server *Server, opts ServerChangeProtectionOpts) (*Action, *Response, error) {
reqBody := schema.ServerActionChangeProtectionRequest{
Rebuild: opts.Rebuild,
Delete: opts.Delete,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/change_protection", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionChangeProtectionResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// ServerRequestConsoleResult is the result of requesting a WebSocket VNC console.
type ServerRequestConsoleResult struct {
Action *Action
WSSURL string
Password string
}
// RequestConsole requests a WebSocket VNC console.
func (c *ServerClient) RequestConsole(ctx context.Context, server *Server) (ServerRequestConsoleResult, *Response, error) {
path := fmt.Sprintf("/servers/%d/actions/request_console", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, nil)
if err != nil {
return ServerRequestConsoleResult{}, nil, err
}
respBody := schema.ServerActionRequestConsoleResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return ServerRequestConsoleResult{}, resp, err
}
return ServerRequestConsoleResult{
Action: ActionFromSchema(respBody.Action),
WSSURL: respBody.WSSURL,
Password: respBody.Password,
}, resp, nil
}
// ServerAttachToNetworkOpts specifies options for attaching a server to a network.
type ServerAttachToNetworkOpts struct {
Network *Network
IP net.IP
AliasIPs []net.IP
}
// AttachToNetwork attaches a server to a network.
func (c *ServerClient) AttachToNetwork(ctx context.Context, server *Server, opts ServerAttachToNetworkOpts) (*Action, *Response, error) {
reqBody := schema.ServerActionAttachToNetworkRequest{
Network: opts.Network.ID,
}
if opts.IP != nil {
reqBody.IP = String(opts.IP.String())
}
for _, aliasIP := range opts.AliasIPs {
reqBody.AliasIPs = append(reqBody.AliasIPs, String(aliasIP.String()))
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/attach_to_network", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionAttachToNetworkResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// ServerDetachFromNetworkOpts specifies options for detaching a server from a network.
type ServerDetachFromNetworkOpts struct {
Network *Network
}
// DetachFromNetwork detaches a server from a network.
func (c *ServerClient) DetachFromNetwork(ctx context.Context, server *Server, opts ServerDetachFromNetworkOpts) (*Action, *Response, error) {
reqBody := schema.ServerActionDetachFromNetworkRequest{
Network: opts.Network.ID,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/detach_from_network", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionDetachFromNetworkResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// ServerChangeAliasIPsOpts specifies options for changing the alias ips of an already attached network.
type ServerChangeAliasIPsOpts struct {
Network *Network
AliasIPs []net.IP
}
// ChangeAliasIPs changes a server's alias IPs in a network.
func (c *ServerClient) ChangeAliasIPs(ctx context.Context, server *Server, opts ServerChangeAliasIPsOpts) (*Action, *Response, error) {
reqBody := schema.ServerActionChangeAliasIPsRequest{
Network: opts.Network.ID,
AliasIPs: []string{},
}
for _, aliasIP := range opts.AliasIPs {
reqBody.AliasIPs = append(reqBody.AliasIPs, aliasIP.String())
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/servers/%d/actions/change_alias_ips", server.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.ServerActionDetachFromNetworkResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}

View file

@ -0,0 +1,149 @@
package hcloud
import (
"context"
"fmt"
"net/url"
"strconv"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// ServerType represents a server type in the Hetzner Cloud.
type ServerType struct {
ID int
Name string
Description string
Cores int
Memory float32
Disk int
StorageType StorageType
CPUType CPUType
Pricings []ServerTypeLocationPricing
}
// StorageType specifies the type of storage.
type StorageType string
const (
// StorageTypeLocal is the type for local storage.
StorageTypeLocal StorageType = "local"
// StorageTypeCeph is the type for remote storage.
StorageTypeCeph StorageType = "ceph"
)
// CPUType specifies the type of the CPU.
type CPUType string
const (
// CPUTypeShared is the type for shared CPU.
CPUTypeShared CPUType = "shared"
//CPUTypeDedicated is the type for dedicated CPU.
CPUTypeDedicated CPUType = "dedicated"
)
// ServerTypeClient is a client for the server types API.
type ServerTypeClient struct {
client *Client
}
// GetByID retrieves a server type by its ID. If the server type does not exist, nil is returned.
func (c *ServerTypeClient) GetByID(ctx context.Context, id int) (*ServerType, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/server_types/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.ServerTypeGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return ServerTypeFromSchema(body.ServerType), resp, nil
}
// GetByName retrieves a server type by its name. If the server type does not exist, nil is returned.
func (c *ServerTypeClient) GetByName(ctx context.Context, name string) (*ServerType, *Response, error) {
if name == "" {
return nil, nil, nil
}
serverTypes, response, err := c.List(ctx, ServerTypeListOpts{Name: name})
if len(serverTypes) == 0 {
return nil, response, err
}
return serverTypes[0], response, err
}
// Get retrieves a server type by its ID if the input can be parsed as an integer, otherwise it
// retrieves a server type by its name. If the server type does not exist, nil is returned.
func (c *ServerTypeClient) Get(ctx context.Context, idOrName string) (*ServerType, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// ServerTypeListOpts specifies options for listing server types.
type ServerTypeListOpts struct {
ListOpts
Name string
}
func (l ServerTypeListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
return vals
}
// List returns a list of server types for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *ServerTypeClient) List(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, *Response, error) {
path := "/server_types?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.ServerTypeListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
serverTypes := make([]*ServerType, 0, len(body.ServerTypes))
for _, s := range body.ServerTypes {
serverTypes = append(serverTypes, ServerTypeFromSchema(s))
}
return serverTypes, resp, nil
}
// All returns all server types.
func (c *ServerTypeClient) All(ctx context.Context) ([]*ServerType, error) {
allServerTypes := []*ServerType{}
opts := ServerTypeListOpts{}
opts.PerPage = 50
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
serverTypes, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allServerTypes = append(allServerTypes, serverTypes...)
return resp, nil
})
if err != nil {
return nil, err
}
return allServerTypes, nil
}

View file

@ -0,0 +1,233 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// SSHKey represents a SSH key in the Hetzner Cloud.
type SSHKey struct {
ID int
Name string
Fingerprint string
PublicKey string
Labels map[string]string
Created time.Time
}
// SSHKeyClient is a client for the SSH keys API.
type SSHKeyClient struct {
client *Client
}
// GetByID retrieves a SSH key by its ID. If the SSH key does not exist, nil is returned.
func (c *SSHKeyClient) GetByID(ctx context.Context, id int) (*SSHKey, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/ssh_keys/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.SSHKeyGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return SSHKeyFromSchema(body.SSHKey), resp, nil
}
// GetByName retrieves a SSH key by its name. If the SSH key does not exist, nil is returned.
func (c *SSHKeyClient) GetByName(ctx context.Context, name string) (*SSHKey, *Response, error) {
if name == "" {
return nil, nil, nil
}
sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Name: name})
if len(sshKeys) == 0 {
return nil, response, err
}
return sshKeys[0], response, err
}
// GetByFingerprint retreives a SSH key by its fingerprint. If the SSH key does not exist, nil is returned.
func (c *SSHKeyClient) GetByFingerprint(ctx context.Context, fingerprint string) (*SSHKey, *Response, error) {
sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Fingerprint: fingerprint})
if len(sshKeys) == 0 {
return nil, response, err
}
return sshKeys[0], response, err
}
// Get retrieves a SSH key by its ID if the input can be parsed as an integer, otherwise it
// retrieves a SSH key by its name. If the SSH key does not exist, nil is returned.
func (c *SSHKeyClient) Get(ctx context.Context, idOrName string) (*SSHKey, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// SSHKeyListOpts specifies options for listing SSH keys.
type SSHKeyListOpts struct {
ListOpts
Name string
Fingerprint string
}
func (l SSHKeyListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
if l.Fingerprint != "" {
vals.Add("fingerprint", l.Fingerprint)
}
return vals
}
// List returns a list of SSH keys for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *SSHKeyClient) List(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, *Response, error) {
path := "/ssh_keys?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.SSHKeyListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
sshKeys := make([]*SSHKey, 0, len(body.SSHKeys))
for _, s := range body.SSHKeys {
sshKeys = append(sshKeys, SSHKeyFromSchema(s))
}
return sshKeys, resp, nil
}
// All returns all SSH keys.
func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) {
return c.AllWithOpts(ctx, SSHKeyListOpts{ListOpts: ListOpts{PerPage: 50}})
}
// AllWithOpts returns all SSH keys with the given options.
func (c *SSHKeyClient) AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) {
allSSHKeys := []*SSHKey{}
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
sshKeys, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allSSHKeys = append(allSSHKeys, sshKeys...)
return resp, nil
})
if err != nil {
return nil, err
}
return allSSHKeys, nil
}
// SSHKeyCreateOpts specifies parameters for creating a SSH key.
type SSHKeyCreateOpts struct {
Name string
PublicKey string
Labels map[string]string
}
// Validate checks if options are valid.
func (o SSHKeyCreateOpts) Validate() error {
if o.Name == "" {
return errors.New("missing name")
}
if o.PublicKey == "" {
return errors.New("missing public key")
}
return nil
}
// Create creates a new SSH key with the given options.
func (c *SSHKeyClient) Create(ctx context.Context, opts SSHKeyCreateOpts) (*SSHKey, *Response, error) {
if err := opts.Validate(); err != nil {
return nil, nil, err
}
reqBody := schema.SSHKeyCreateRequest{
Name: opts.Name,
PublicKey: opts.PublicKey,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/ssh_keys", bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.SSHKeyCreateResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return SSHKeyFromSchema(respBody.SSHKey), resp, nil
}
// Delete deletes a SSH key.
func (c *SSHKeyClient) Delete(ctx context.Context, sshKey *SSHKey) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/ssh_keys/%d", sshKey.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}
// SSHKeyUpdateOpts specifies options for updating a SSH key.
type SSHKeyUpdateOpts struct {
Name string
Labels map[string]string
}
// Update updates a SSH key.
func (c *SSHKeyClient) Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUpdateOpts) (*SSHKey, *Response, error) {
reqBody := schema.SSHKeyUpdateRequest{
Name: opts.Name,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/ssh_keys/%d", sshKey.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.SSHKeyUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return SSHKeyFromSchema(respBody.SSHKey), resp, nil
}

View file

@ -0,0 +1,399 @@
package hcloud
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"time"
"github.com/hetznercloud/hcloud-go/hcloud/schema"
)
// Volume represents a volume in the Hetzner Cloud.
type Volume struct {
ID int
Name string
Status VolumeStatus
Server *Server
Location *Location
Size int
Protection VolumeProtection
Labels map[string]string
LinuxDevice string
Created time.Time
}
// VolumeProtection represents the protection level of a volume.
type VolumeProtection struct {
Delete bool
}
// VolumeClient is a client for the volume API.
type VolumeClient struct {
client *Client
}
// VolumeStatus specifies a volume's status.
type VolumeStatus string
const (
// VolumeStatusCreating is the status when a volume is being created.
VolumeStatusCreating VolumeStatus = "creating"
// VolumeStatusAvailable is the status when a volume is available.
VolumeStatusAvailable VolumeStatus = "available"
)
// GetByID retrieves a volume by its ID. If the volume does not exist, nil is returned.
func (c *VolumeClient) GetByID(ctx context.Context, id int) (*Volume, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/volumes/%d", id), nil)
if err != nil {
return nil, nil, err
}
var body schema.VolumeGetResponse
resp, err := c.client.Do(req, &body)
if err != nil {
if IsError(err, ErrorCodeNotFound) {
return nil, resp, nil
}
return nil, nil, err
}
return VolumeFromSchema(body.Volume), resp, nil
}
// GetByName retrieves a volume by its name. If the volume does not exist, nil is returned.
func (c *VolumeClient) GetByName(ctx context.Context, name string) (*Volume, *Response, error) {
if name == "" {
return nil, nil, nil
}
volumes, response, err := c.List(ctx, VolumeListOpts{Name: name})
if len(volumes) == 0 {
return nil, response, err
}
return volumes[0], response, err
}
// Get retrieves a volume by its ID if the input can be parsed as an integer, otherwise it
// retrieves a volume by its name. If the volume does not exist, nil is returned.
func (c *VolumeClient) Get(ctx context.Context, idOrName string) (*Volume, *Response, error) {
if id, err := strconv.Atoi(idOrName); err == nil {
return c.GetByID(ctx, int(id))
}
return c.GetByName(ctx, idOrName)
}
// VolumeListOpts specifies options for listing volumes.
type VolumeListOpts struct {
ListOpts
Name string
Status []VolumeStatus
}
func (l VolumeListOpts) values() url.Values {
vals := l.ListOpts.values()
if l.Name != "" {
vals.Add("name", l.Name)
}
for _, status := range l.Status {
vals.Add("status", string(status))
}
return vals
}
// List returns a list of volumes for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *VolumeClient) List(ctx context.Context, opts VolumeListOpts) ([]*Volume, *Response, error) {
path := "/volumes?" + opts.values().Encode()
req, err := c.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
var body schema.VolumeListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, nil, err
}
volumes := make([]*Volume, 0, len(body.Volumes))
for _, s := range body.Volumes {
volumes = append(volumes, VolumeFromSchema(s))
}
return volumes, resp, nil
}
// All returns all volumes.
func (c *VolumeClient) All(ctx context.Context) ([]*Volume, error) {
return c.AllWithOpts(ctx, VolumeListOpts{ListOpts: ListOpts{PerPage: 50}})
}
// AllWithOpts returns all volumes with the given options.
func (c *VolumeClient) AllWithOpts(ctx context.Context, opts VolumeListOpts) ([]*Volume, error) {
allVolumes := []*Volume{}
_, err := c.client.all(func(page int) (*Response, error) {
opts.Page = page
volumes, resp, err := c.List(ctx, opts)
if err != nil {
return resp, err
}
allVolumes = append(allVolumes, volumes...)
return resp, nil
})
if err != nil {
return nil, err
}
return allVolumes, nil
}
// VolumeCreateOpts specifies parameters for creating a volume.
type VolumeCreateOpts struct {
Name string
Size int
Server *Server
Location *Location
Labels map[string]string
Automount *bool
Format *string
}
// Validate checks if options are valid.
func (o VolumeCreateOpts) Validate() error {
if o.Name == "" {
return errors.New("missing name")
}
if o.Size <= 0 {
return errors.New("size must be greater than 0")
}
if o.Server == nil && o.Location == nil {
return errors.New("one of server or location must be provided")
}
if o.Server != nil && o.Location != nil {
return errors.New("only one of server or location must be provided")
}
if o.Server == nil && (o.Automount != nil && *o.Automount) {
return errors.New("server must be provided when automount is true")
}
return nil
}
// VolumeCreateResult is the result of creating a volume.
type VolumeCreateResult struct {
Volume *Volume
Action *Action
NextActions []*Action
}
// Create creates a new volume with the given options.
func (c *VolumeClient) Create(ctx context.Context, opts VolumeCreateOpts) (VolumeCreateResult, *Response, error) {
if err := opts.Validate(); err != nil {
return VolumeCreateResult{}, nil, err
}
reqBody := schema.VolumeCreateRequest{
Name: opts.Name,
Size: opts.Size,
Automount: opts.Automount,
Format: opts.Format,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
if opts.Server != nil {
reqBody.Server = Int(opts.Server.ID)
}
if opts.Location != nil {
if opts.Location.ID != 0 {
reqBody.Location = opts.Location.ID
} else {
reqBody.Location = opts.Location.Name
}
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return VolumeCreateResult{}, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/volumes", bytes.NewReader(reqBodyData))
if err != nil {
return VolumeCreateResult{}, nil, err
}
var respBody schema.VolumeCreateResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return VolumeCreateResult{}, resp, err
}
var action *Action
if respBody.Action != nil {
action = ActionFromSchema(*respBody.Action)
}
return VolumeCreateResult{
Volume: VolumeFromSchema(respBody.Volume),
Action: action,
NextActions: ActionsFromSchema(respBody.NextActions),
}, resp, nil
}
// Delete deletes a volume.
func (c *VolumeClient) Delete(ctx context.Context, volume *Volume) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/volumes/%d", volume.ID), nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}
// VolumeUpdateOpts specifies options for updating a volume.
type VolumeUpdateOpts struct {
Name string
Labels map[string]string
}
// Update updates a volume.
func (c *VolumeClient) Update(ctx context.Context, volume *Volume, opts VolumeUpdateOpts) (*Volume, *Response, error) {
reqBody := schema.VolumeUpdateRequest{
Name: opts.Name,
}
if opts.Labels != nil {
reqBody.Labels = &opts.Labels
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/volumes/%d", volume.ID)
req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.VolumeUpdateResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return VolumeFromSchema(respBody.Volume), resp, nil
}
// VolumeAttachOpts specifies options for attaching a volume.
type VolumeAttachOpts struct {
Server *Server
Automount *bool
}
// AttachWithOpts attaches a volume to a server.
func (c *VolumeClient) AttachWithOpts(ctx context.Context, volume *Volume, opts VolumeAttachOpts) (*Action, *Response, error) {
reqBody := schema.VolumeActionAttachVolumeRequest{
Server: opts.Server.ID,
Automount: opts.Automount,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/volumes/%d/actions/attach", volume.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.VolumeActionAttachVolumeResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// Attach attaches a volume to a server.
func (c *VolumeClient) Attach(ctx context.Context, volume *Volume, server *Server) (*Action, *Response, error) {
return c.AttachWithOpts(ctx, volume, VolumeAttachOpts{Server: server})
}
// Detach detaches a volume from a server.
func (c *VolumeClient) Detach(ctx context.Context, volume *Volume) (*Action, *Response, error) {
var reqBody schema.VolumeActionDetachVolumeRequest
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/volumes/%d/actions/detach", volume.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.VolumeActionDetachVolumeResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}
// VolumeChangeProtectionOpts specifies options for changing the resource protection level of a volume.
type VolumeChangeProtectionOpts struct {
Delete *bool
}
// ChangeProtection changes the resource protection level of a volume.
func (c *VolumeClient) ChangeProtection(ctx context.Context, volume *Volume, opts VolumeChangeProtectionOpts) (*Action, *Response, error) {
reqBody := schema.VolumeActionChangeProtectionRequest{
Delete: opts.Delete,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/volumes/%d/actions/change_protection", volume.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.VolumeActionChangeProtectionResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}
// Resize changes the size of a volume.
func (c *VolumeClient) Resize(ctx context.Context, volume *Volume, size int) (*Action, *Response, error) {
reqBody := schema.VolumeActionResizeVolumeRequest{
Size: size,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
path := fmt.Sprintf("/volumes/%d/actions/resize", volume.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
respBody := schema.VolumeActionResizeVolumeResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, err
}

6
vendor/modules.txt vendored
View file

@ -230,7 +230,7 @@ github.com/golang/protobuf/ptypes/wrappers
# github.com/golang/snappy v0.0.1
## explicit
github.com/golang/snappy
# github.com/google/go-cmp v0.4.0
# github.com/google/go-cmp v0.5.0
github.com/google/go-cmp/cmp
github.com/google/go-cmp/cmp/internal/diff
github.com/google/go-cmp/cmp/internal/flags
@ -298,6 +298,10 @@ github.com/hashicorp/golang-lru
github.com/hashicorp/golang-lru/simplelru
# github.com/hashicorp/serf v0.9.0
github.com/hashicorp/serf/coordinate
# github.com/hetznercloud/hcloud-go v1.21.1
## explicit
github.com/hetznercloud/hcloud-go/hcloud
github.com/hetznercloud/hcloud-go/hcloud/schema
# github.com/influxdata/influxdb v1.8.1
## explicit
github.com/influxdata/influxdb/client/v2