From b6955bf1cadde44cecd7f1e67de7dc575e5afc08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Fri, 21 Aug 2020 15:49:19 +0200 Subject: [PATCH] Add hetzner service discovery (#7822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Kämmerling --- config/config_test.go | 35 +- config/testdata/conf.good.yml | 9 + config/testdata/hetzner_role.bad.yml | 4 + discovery/hetzner/hcloud.go | 132 +++ discovery/hetzner/hcloud_test.go | 124 +++ discovery/hetzner/hetzner.go | 150 +++ discovery/hetzner/mock_test.go | 552 ++++++++++ discovery/hetzner/robot.go | 124 +++ discovery/hetzner/robot_test.go | 86 ++ discovery/install/install.go | 1 + docs/configuration/configuration.md | 83 ++ documentation/examples/prometheus-hetzner.yml | 47 + go.mod | 1 + go.sum | 4 + .../github.com/google/go-cmp/cmp/compare.go | 83 +- .../google/go-cmp/cmp/export_panic.go | 2 +- .../google/go-cmp/cmp/export_unsafe.go | 20 +- .../google/go-cmp/cmp/internal/diff/diff.go | 22 +- .../google/go-cmp/cmp/internal/value/name.go | 157 +++ .../cmp/internal/value/pointer_purego.go | 10 + .../cmp/internal/value/pointer_unsafe.go | 10 + vendor/github.com/google/go-cmp/cmp/path.go | 7 +- vendor/github.com/google/go-cmp/cmp/report.go | 5 +- .../google/go-cmp/cmp/report_compare.go | 200 +++- .../google/go-cmp/cmp/report_references.go | 264 +++++ .../google/go-cmp/cmp/report_reflect.go | 241 +++-- .../google/go-cmp/cmp/report_slices.go | 135 ++- .../google/go-cmp/cmp/report_text.go | 86 +- .../github.com/hetznercloud/hcloud-go/LICENSE | 21 + .../hetznercloud/hcloud-go/hcloud/action.go | 210 ++++ .../hcloud-go/hcloud/certificate.go | 246 +++++ .../hetznercloud/hcloud-go/hcloud/client.go | 394 ++++++++ .../hcloud-go/hcloud/datacenter.go | 129 +++ .../hetznercloud/hcloud-go/hcloud/error.go | 62 ++ .../hcloud-go/hcloud/floating_ip.go | 378 +++++++ .../hetznercloud/hcloud-go/hcloud/hcloud.go | 5 + .../hetznercloud/hcloud-go/hcloud/helper.go | 18 + .../hetznercloud/hcloud-go/hcloud/image.go | 268 +++++ .../hetznercloud/hcloud-go/hcloud/iso.go | 139 +++ .../hcloud-go/hcloud/load_balancer.go | 933 +++++++++++++++++ .../hcloud-go/hcloud/load_balancer_type.go | 126 +++ .../hetznercloud/hcloud-go/hcloud/location.go | 126 +++ .../hetznercloud/hcloud-go/hcloud/network.go | 454 +++++++++ .../hetznercloud/hcloud-go/hcloud/pricing.go | 95 ++ .../hetznercloud/hcloud-go/hcloud/schema.go | 891 ++++++++++++++++ .../hcloud-go/hcloud/schema/action.go | 39 + .../hcloud-go/hcloud/schema/certificate.go | 52 + .../hcloud-go/hcloud/schema/datacenter.go | 23 + .../hcloud-go/hcloud/schema/error.go | 43 + .../hcloud-go/hcloud/schema/floating_ip.go | 118 +++ .../hcloud-go/hcloud/schema/image.go | 68 ++ .../hcloud-go/hcloud/schema/iso.go | 22 + .../hcloud-go/hcloud/schema/load_balancer.go | 386 +++++++ .../hcloud/schema/load_balancer_type.go | 25 + .../hcloud-go/hcloud/schema/location.go | 23 + .../hcloud-go/hcloud/schema/meta.go | 23 + .../hcloud-go/hcloud/schema/network.go | 150 +++ .../hcloud-go/hcloud/schema/pricing.go | 74 ++ .../hcloud-go/hcloud/schema/server.go | 366 +++++++ .../hcloud-go/hcloud/schema/server_type.go | 26 + .../hcloud-go/hcloud/schema/ssh_key.go | 50 + .../hcloud-go/hcloud/schema/volume.go | 110 ++ .../hetznercloud/hcloud-go/hcloud/server.go | 953 ++++++++++++++++++ .../hcloud-go/hcloud/server_type.go | 149 +++ .../hetznercloud/hcloud-go/hcloud/ssh_key.go | 233 +++++ .../hetznercloud/hcloud-go/hcloud/volume.go | 399 ++++++++ vendor/modules.txt | 6 +- 67 files changed, 10240 insertions(+), 187 deletions(-) create mode 100644 config/testdata/hetzner_role.bad.yml create mode 100644 discovery/hetzner/hcloud.go create mode 100644 discovery/hetzner/hcloud_test.go create mode 100644 discovery/hetzner/hetzner.go create mode 100644 discovery/hetzner/mock_test.go create mode 100644 discovery/hetzner/robot.go create mode 100644 discovery/hetzner/robot_test.go create mode 100644 documentation/examples/prometheus-hetzner.yml create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/value/name.go create mode 100644 vendor/github.com/google/go-cmp/cmp/report_references.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/LICENSE create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/certificate.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer_type.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/network.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/certificate.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer_type.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/network.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go diff --git a/config/config_test.go b/config/config_test.go index da25e86a1..70ed634fa 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -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) { diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index 7d0b441ef..fc2db1c06 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -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 diff --git a/config/testdata/hetzner_role.bad.yml b/config/testdata/hetzner_role.bad.yml new file mode 100644 index 000000000..0a5cc8c48 --- /dev/null +++ b/config/testdata/hetzner_role.bad.yml @@ -0,0 +1,4 @@ +scrape_configs: +- hetzner_sd_configs: + - role: invalid + diff --git a/discovery/hetzner/hcloud.go b/discovery/hetzner/hcloud.go new file mode 100644 index 000000000..66b96e63d --- /dev/null +++ b/discovery/hetzner/hcloud.go @@ -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 +} diff --git a/discovery/hetzner/hcloud_test.go b/discovery/hetzner/hcloud_test.go new file mode 100644 index 000000000..96fdc0262 --- /dev/null +++ b/discovery/hetzner/hcloud_test.go @@ -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]) + }) + } +} diff --git a/discovery/hetzner/hetzner.go b/discovery/hetzner/hetzner.go new file mode 100644 index 000000000..5b727464c --- /dev/null +++ b/discovery/hetzner/hetzner.go @@ -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") +} diff --git a/discovery/hetzner/mock_test.go b/discovery/hetzner/mock_test.go new file mode 100644 index 000000000..6c374b4bc --- /dev/null +++ b/discovery/hetzner/mock_test.go @@ -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 + } + } +]`, + ) + }) +} diff --git a/discovery/hetzner/robot.go b/discovery/hetzner/robot.go new file mode 100644 index 000000000..9382dd4ee --- /dev/null +++ b/discovery/hetzner/robot.go @@ -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"` +} diff --git a/discovery/hetzner/robot_test.go b/discovery/hetzner/robot_test.go new file mode 100644 index 000000000..e483ebb28 --- /dev/null +++ b/discovery/hetzner/robot_test.go @@ -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]) + }) + } +} diff --git a/discovery/install/install.go b/discovery/install/install.go index 709f8694e..aa07b26ea 100644 --- a/discovery/install/install.go +++ b/discovery/install/install.go @@ -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 diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index c05049d23..5b4ebe33e 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -215,6 +215,10 @@ file_sd_configs: gce_sd_configs: [ - ... ] +# List of Hetzner service discovery configurations. +hetzner_sd_configs: + [ - ... ] + # List of Kubernetes service discovery configurations. kubernetes_sd_configs: [ - ... ] @@ -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 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_`: the private ipv4 of the server within the network named in the metric if it is attached to a network +* `__meta_hetzner_hcloud_label_`: 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: + +# 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: ] + [ password: ] + [ password_file: ] + +# Optional bearer token authentication information, required when role is hcloud +# Role robot does not support bearer token authentication. +[ bearer_token: ] + +# Optional bearer token file authentication information. +[ bearer_token_file: ] + +# Optional proxy URL. +[ proxy_url: ] + +# TLS configuration. +tls_config: + [ ] + +# The port to scrape metrics from. +[ port: | default = 80 ] + +# The time after which the servers are refreshed. +[ refresh_interval: | default = 60s ] +``` + ### `` Kubernetes SD configurations allow retrieving scrape targets from @@ -1494,6 +1573,10 @@ dockerswarm_sd_configs: gce_sd_configs: [ - ... ] +# List of Hetzner service discovery configurations. +hetzner_sd_configs: + [ - ... ] + # List of Kubernetes service discovery configurations. kubernetes_sd_configs: [ - ... ] diff --git a/documentation/examples/prometheus-hetzner.yml b/documentation/examples/prometheus-hetzner.yml new file mode 100644 index 000000000..158327bf1 --- /dev/null +++ b/documentation/examples/prometheus-hetzner.yml @@ -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: "" + 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: "" + 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: "" + 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' diff --git a/go.mod b/go.mod index 072b3eaa8..b3bf15c21 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ccc49d9ae..1ef02a767 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/vendor/github.com/google/go-cmp/cmp/compare.go b/vendor/github.com/google/go-cmp/cmp/compare.go index c9a63ceda..580ae2097 100644 --- a/vendor/github.com/google/go-cmp/cmp/compare.go +++ b/vendor/github.com/google/go-cmp/cmp/compare.go @@ -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) diff --git a/vendor/github.com/google/go-cmp/cmp/export_panic.go b/vendor/github.com/google/go-cmp/cmp/export_panic.go index dd032354f..dfa5d2137 100644 --- a/vendor/github.com/google/go-cmp/cmp/export_panic.go +++ b/vendor/github.com/google/go-cmp/cmp/export_panic.go @@ -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") } diff --git a/vendor/github.com/google/go-cmp/cmp/export_unsafe.go b/vendor/github.com/google/go-cmp/cmp/export_unsafe.go index 57020e26c..351f1a34b 100644 --- a/vendor/github.com/google/go-cmp/cmp/export_unsafe.go +++ b/vendor/github.com/google/go-cmp/cmp/export_unsafe.go @@ -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 } diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go index 3d2e42662..730e223ee 100644 --- a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go @@ -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} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/name.go b/vendor/github.com/google/go-cmp/cmp/internal/value/name.go new file mode 100644 index 000000000..8228e7d51 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/name.go @@ -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 +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go index 0a01c4796..e9e384a1c 100644 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go @@ -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 +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go index da134ae2a..b50c17ec7 100644 --- a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go @@ -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) +} diff --git a/vendor/github.com/google/go-cmp/cmp/path.go b/vendor/github.com/google/go-cmp/cmp/path.go index 509d6b852..603dbb002 100644 --- a/vendor/github.com/google/go-cmp/cmp/path.go +++ b/vendor/github.com/google/go-cmp/cmp/path.go @@ -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 diff --git a/vendor/github.com/google/go-cmp/cmp/report.go b/vendor/github.com/google/go-cmp/cmp/report.go index 6ddf29993..aafcb3635 100644 --- a/vendor/github.com/google/go-cmp/cmp/report.go +++ b/vendor/github.com/google/go-cmp/cmp/report.go @@ -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) { diff --git a/vendor/github.com/google/go-cmp/cmp/report_compare.go b/vendor/github.com/google/go-cmp/cmp/report_compare.go index 17a05eede..9e2180964 100644 --- a/vendor/github.com/google/go-cmp/cmp/report_compare.go +++ b/vendor/github.com/google/go-cmp/cmp/report_compare.go @@ -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 diff --git a/vendor/github.com/google/go-cmp/cmp/report_references.go b/vendor/github.com/google/go-cmp/cmp/report_references.go new file mode 100644 index 000000000..d620c2c20 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report_references.go @@ -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 +} diff --git a/vendor/github.com/google/go-cmp/cmp/report_reflect.go b/vendor/github.com/google/go-cmp/cmp/report_reflect.go index 2761b6289..2d722ea51 100644 --- a/vendor/github.com/google/go-cmp/cmp/report_reflect.go +++ b/vendor/github.com/google/go-cmp/cmp/report_reflect.go @@ -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 -} diff --git a/vendor/github.com/google/go-cmp/cmp/report_slices.go b/vendor/github.com/google/go-cmp/cmp/report_slices.go index eafcf2e4c..35315dad3 100644 --- a/vendor/github.com/google/go-cmp/cmp/report_slices.go +++ b/vendor/github.com/google/go-cmp/cmp/report_slices.go @@ -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 } diff --git a/vendor/github.com/google/go-cmp/cmp/report_text.go b/vendor/github.com/google/go-cmp/cmp/report_text.go index 8b8fcab7b..8b12c05cd 100644 --- a/vendor/github.com/google/go-cmp/cmp/report_text.go +++ b/vendor/github.com/google/go-cmp/cmp/report_text.go @@ -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 } diff --git a/vendor/github.com/hetznercloud/hcloud-go/LICENSE b/vendor/github.com/hetznercloud/hcloud-go/LICENSE new file mode 100644 index 000000000..394ce101f --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/LICENSE @@ -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. diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go new file mode 100644 index 000000000..1a62f8bab --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/certificate.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/certificate.go new file mode 100644 index 000000000..5d34f70e1 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/certificate.go @@ -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) +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go new file mode 100644 index 000000000..b03d45f69 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go new file mode 100644 index 000000000..272cd6aef --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go new file mode 100644 index 000000000..9eeab771d --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go new file mode 100644 index 000000000..025351f40 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go new file mode 100644 index 000000000..23384b46f --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go @@ -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" diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go new file mode 100644 index 000000000..e8e264267 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go @@ -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 } diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go new file mode 100644 index 000000000..a0aa18f10 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go new file mode 100644 index 000000000..18e161041 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer.go new file mode 100644 index 000000000..79fba32e6 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer_type.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer_type.go new file mode 100644 index 000000000..12202d02b --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/load_balancer_type.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go new file mode 100644 index 000000000..ad2fff854 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/network.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/network.go new file mode 100644 index 000000000..4b72679fb --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/network.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go new file mode 100644 index 000000000..618907a14 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go new file mode 100644 index 000000000..ba5c1179e --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go new file mode 100644 index 000000000..df4d7cf71 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/certificate.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/certificate.go new file mode 100644 index 000000000..251add4dd --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/certificate.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go new file mode 100644 index 000000000..3e8178e89 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go new file mode 100644 index 000000000..6bcb6ecad --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go new file mode 100644 index 000000000..37295dad9 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go new file mode 100644 index 000000000..c354d9ea9 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go new file mode 100644 index 000000000..e41046896 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer.go new file mode 100644 index 000000000..7c8784feb --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer_type.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer_type.go new file mode 100644 index 000000000..b0baf0489 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/load_balancer_type.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go new file mode 100644 index 000000000..3dd58ad5e --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go new file mode 100644 index 000000000..9b06cda8c --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/network.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/network.go new file mode 100644 index 000000000..e1c97e426 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/network.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go new file mode 100644 index 000000000..074ae4354 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go new file mode 100644 index 000000000..836b3bf36 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go new file mode 100644 index 000000000..5d4f10b03 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go new file mode 100644 index 000000000..f230b3ddd --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go new file mode 100644 index 000000000..ad745ea97 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go @@ -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"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go new file mode 100644 index 000000000..4c53f8e07 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go new file mode 100644 index 000000000..043945f47 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go new file mode 100644 index 000000000..f5be20b8d --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go @@ -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 +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go new file mode 100644 index 000000000..fd32271cf --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 119c11b41..91e203751 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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