pax_global_header00006660000000000000000000000064141444203320014507gustar00rootroot0000000000000052 comment=41fef2fa317e0051cb42fa2a01d1274d8930afe3 golang-github-hetznercloud-hcloud-go-1.33.1/000077500000000000000000000000001414442033200207065ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/.errcheck_excludes.txt000066400000000000000000000000001414442033200251750ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/.github/000077500000000000000000000000001414442033200222465ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/.github/workflows/000077500000000000000000000000001414442033200243035ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/.github/workflows/ci.yml000066400000000000000000000007341414442033200254250ustar00rootroot00000000000000name: Continuous Integration on: [push, pull_request] jobs: build: name: Build runs-on: ubuntu-latest strategy: matrix: go-version: [1.13, 1.14, 1.15, 1.16, 1.17] steps: - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Check out code into the Go module directory uses: actions/checkout@v1 - name: Run tests run: go test -v ./... golang-github-hetznercloud-hcloud-go-1.33.1/.github/workflows/golangci-lint.yml000066400000000000000000000016441414442033200275620ustar00rootroot00000000000000name: golangci-lint on: push: tags: - v* branches: - master - main pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: latest # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true # Optional: if set to true then the action will use pre-installed Go # skip-go-installation: true golang-github-hetznercloud-hcloud-go-1.33.1/.github/workflows/release.yml000066400000000000000000000010731414442033200264470ustar00rootroot00000000000000name: Release Changelog on: push: tags: - '*' jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: distribution: goreleaser version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} golang-github-hetznercloud-hcloud-go-1.33.1/.gitignore000066400000000000000000000000061414442033200226720ustar00rootroot00000000000000dist/ golang-github-hetznercloud-hcloud-go-1.33.1/.gitlab-ci.yml000066400000000000000000000006021414442033200233400ustar00rootroot00000000000000stages: - test variables: GIT_SUBMODULE_STRATEGY: normal test:golangci-lint: stage: test image: golangci/golangci-lint:latest script: - golangci-lint run -v except: - tags - master tags: - hc-bladerunner test:tests: stage: test image: golang:1.17 script: - go test -v -race ./... except: - tags - master tags: - hc-bladerunner golang-github-hetznercloud-hcloud-go-1.33.1/.golangci.yaml000066400000000000000000000025261414442033200234400ustar00rootroot00000000000000--- linters-settings: errcheck: exclude: ./.errcheck_excludes.txt exhaustive: default-signifies-exhaustive: true gomodguard: blocked: modules: - github.com/tj/assert: recommendations: - github.com/stretchr/testify/assert reason: | One assertion library is enough and we use testify/assert everywhere. - github.com/magiconair/properties: recommendations: - github.com/stretchr/testify/assert reason: > We do not currently need to parse properties files. At the same time this module has an assert package which tends to get imported by accident. It is therefore blocked. misspell: locale: "US" linters: disable-all: true enable: - bodyclose - deadcode - depguard - errcheck - exhaustive - gocritic - goimports - golint - gomodguard - gosec - gosimple - govet - ineffassign - misspell - prealloc - rowserrcheck - scopelint - staticcheck - structcheck - typecheck - unparam - unused - varcheck - whitespace issues: exclude-rules: - path: _test\.go linters: - gosec - scopelint - errcheck - linters: - gosec text: "G204:" golang-github-hetznercloud-hcloud-go-1.33.1/.goreleaser.yml000066400000000000000000000001501414442033200236330ustar00rootroot00000000000000builds: - skip: true changelog: sort: asc filters: exclude: - '^docs:' - '^test:' golang-github-hetznercloud-hcloud-go-1.33.1/CHANGES.md000066400000000000000000000103461414442033200223040ustar00rootroot00000000000000# Changes As of release v1.24.0 we moved the release notes to Github Releases: https://github.com/hetznercloud/hcloud-go/releases ## v1.23.1 * Add removed `ErrorCodeServerAlreadyAttached` again ## v1.23.0 * Add missing constants for all resource specific error codes * Expose metrics for Servers and Load Balancers * Add support for vSwitch Subnetworks ## v1.22.0 * Add `PrimaryDiskSize` Field to `Server` ## v1.21.1 * Don't send `Authorization` Header when `WithToken` was not called ## v1.21.0 * Add `IncludeDeprecated` Field to `ImageListOpts` ## v1.20.0 * Add support for Load Balancer Label Selector targets * Add support for Load Balancer IP targets ## v1.19.0 * Fix nil pointer dereference when creating a Load Balancer with HTTP(S) service and not providing HTTP-specific options * Add `IncludedTraffic`, `OutgoingTraffic` and `IngoingTraffic` fields to `LoadBalancer` * Add `ChangeType()` method to the Load Balancer client * Fix retrying of requests that contain a body ## v1.18.2 * Retry API requests on conflict error ## v1.18.1 * Make all `GetByName` methods return `nil` when an empty name is provided * Clarify that filters specified in options for List() calls are not taken into account when their value corresponds to their zero value or when they are empty. ## v1.18.0 * Add `Status` field to `Volume` * Add subnet type `cloud` * Add `WithHTTPClient` option to specify a custom `http.Client` * Add API for requesting a VNC console * Add support for load balancers and certificates (beta) ## v1.17.0 * Add `Created` field to `SSHKey` ## v1.16.0 * Make IP range optional when adding a subnet to a network * Add support for names to Floating IPs ## v1.15.1 * Rename `MacAddress` to `MACAddress` on `ServerPrivateNet` ## v1.15.0 * Add `MacAddress` field to `ServerPrivateNet` * Add `WithDebugWriter()` client option to provide an `io.Writer` to write debug output to ## v1.14.0 * Add `Created` field to `FloatingIP` * Add support for networks ## v1.13.0 * Add missing fields to `*ListOpts` structs * Fix error handling in `WatchProgress()` * Add support for filtering volumes, images, and servers by status ## v1.12.0 * Add missing constants for all [documented error codes](https://docs.hetzner.cloud/#overview-errors) * Add support for automounting volumes * Add support for attaching volumes when creating a server ## v1.11.0 * Add `NextActions` to `ServerCreateResult` and `VolumeCreateResult` ## v1.10.0 * Add `WithApplication()` client option to provide an application name and version that will be included in the `User-Agent` HTTP header * Add support for volumes ## v1.9.0 * Add `AllWithOpts()` to server, Floating IP, image, and SSH key client * Expose labels of servers, Floating IPs, images, and SSH Keys ## v1.8.0 * Add `WithPollInterval()` option to `Client` which allows to specify the polling interval ([issue #92](https://github.com/hetznercloud/hcloud-go/issues/92)) * Add `CPUType` field to `ServerType` ([issue #91](https://github.com/hetznercloud/hcloud-go/pull/91)) ## v1.7.0 * Add `Deprecated ` field to `Image` ([issue #88](https://github.com/hetznercloud/hcloud-go/issues/88)) * Add `StartAfterCreate` flag to `ServerCreateOpts` ([issue #87](https://github.com/hetznercloud/hcloud-go/issues/87)) * Fix enum types ([issue #89](https://github.com/hetznercloud/hcloud-go/issues/89)) ## v1.6.0 * Add `ChangeProtection()` to server, Floating IP, and image client * Expose protection of servers, Floating IPs, and images ## v1.5.0 * Add `GetByFingerprint()` to SSH key client ## v1.4.0 * Retry all calls that triggered the API ratelimit * Slow down `WatchProgress()` in action client from 100ms polling interval to 500ms ## v1.3.1 * Make clients using the old error code for ratelimiting work as expected ([issue #73](https://github.com/hetznercloud/hcloud-go/issues/73)) ## v1.3.0 * Support passing user data on server creation ([issue #70](https://github.com/hetznercloud/hcloud-go/issues/70)) * Fix leaking response body by not closing it ([issue #68](https://github.com/hetznercloud/hcloud-go/issues/68)) ## v1.2.0 * Add `WatchProgress()` to action client * Use correct error code for ratelimit error (deprecated `ErrorCodeLimitReached`, added `ErrorCodeRateLimitExceeded`) ## v1.1.0 * Add `Image` field to `Server` golang-github-hetznercloud-hcloud-go-1.33.1/LICENSE000066400000000000000000000020701414442033200217120ustar00rootroot00000000000000MIT 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. golang-github-hetznercloud-hcloud-go-1.33.1/README.md000066400000000000000000000022001414442033200221570ustar00rootroot00000000000000# hcloud: A Go library for the Hetzner Cloud API [![GitHub Actions status](https://github.com/hetznercloud/hcloud-go/workflows/Continuous%20Integration/badge.svg)](https://github.com/hetznercloud/hcloud-go/actions) [![GoDoc](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud?status.svg)](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud) Package hcloud is a library for the Hetzner Cloud API. The library’s documentation is available at [GoDoc](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud), the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud/). ## Example ```go package main import ( "context" "fmt" "log" "github.com/hetznercloud/hcloud-go/hcloud" ) func main() { client := hcloud.NewClient(hcloud.WithToken("token")) server, _, err := client.Server.GetByID(context.Background(), 1) if err != nil { log.Fatalf("error retrieving server: %s\n", err) } if server != nil { fmt.Printf("server 1 is called %q\n", server.Name) } else { fmt.Println("server 1 not found") } } ``` ## License MIT license golang-github-hetznercloud-hcloud-go-1.33.1/go.mod000066400000000000000000000015011414442033200220110ustar00rootroot00000000000000module github.com/hetznercloud/hcloud-go go 1.17 require ( github.com/google/go-cmp v0.5.5 github.com/prometheus/client_golang v1.11.0 github.com/stretchr/testify v1.7.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.4.3 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect google.golang.org/protobuf v1.26.0-rc.1 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) golang-github-hetznercloud-hcloud-go-1.33.1/go.sum000066400000000000000000000337541414442033200220550ustar00rootroot00000000000000cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/000077500000000000000000000000001414442033200221645ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/action.go000066400000000000000000000151731414442033200237770ustar00rootroot00000000000000package 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 ID []int Status []ActionStatus Sort []string } func (l ActionListOpts) values() url.Values { vals := l.ListOpts.values() for _, id := range l.ID { vals.Add("id", fmt.Sprintf("%d", id)) } 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 } // AllWithOpts returns all actions for the given options. func (c *ActionClient) AllWithOpts(ctx context.Context, opts ActionListOpts) ([]*Action, error) { allActions := []*Action{} 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 } // WatchOverallProgress watches several actions' progress until they complete with success or error. func (c *ActionClient) WatchOverallProgress(ctx context.Context, actions []*Action) (<-chan int, <-chan error) { errCh := make(chan error, len(actions)) progressCh := make(chan int) go func() { defer close(errCh) defer close(progressCh) successIDs := make([]int, 0, len(actions)) watchIDs := make(map[int]struct{}, len(actions)) for _, action := range actions { watchIDs[action.ID] = struct{}{} } ticker := time.NewTicker(c.client.pollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): errCh <- ctx.Err() return case <-ticker.C: break } opts := ActionListOpts{} for watchID := range watchIDs { opts.ID = append(opts.ID, watchID) } as, err := c.AllWithOpts(ctx, opts) if err != nil { errCh <- err return } for _, a := range as { switch a.Status { case ActionStatusRunning: continue case ActionStatusSuccess: delete(watchIDs, a.ID) successIDs := append(successIDs, a.ID) sendProgress(progressCh, int(float64(len(actions)-len(successIDs))/float64(len(actions))*100)) case ActionStatusError: delete(watchIDs, a.ID) errCh <- fmt.Errorf("action %d failed: %w", a.ID, a.Error()) } } if len(watchIDs) == 0 { return } } }() return progressCh, errCh } // WatchProgress watches one 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) defer ticker.Stop() 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(progressCh, a.Progress) case ActionStatusSuccess: sendProgress(progressCh, 100) errCh <- nil return case ActionStatusError: errCh <- a.Error() return } } }() return progressCh, errCh } func sendProgress(progressCh chan int, p int) { select { case progressCh <- p: break default: break } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/action_test.go000066400000000000000000000143241414442033200250330ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestActionClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(schema.ActionGetResponse{ Action: schema.Action{ ID: 1, Status: "running", Command: "create_server", Progress: 50, Started: time.Date(2017, 12, 4, 14, 31, 1, 0, time.UTC), }, }) }) ctx := context.Background() action, _, err := env.Client.Action.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if action == nil { t.Fatal("no action") } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestActionClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) _ = json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() action, _, err := env.Client.Action.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if action != nil { t.Fatal("expected no action") } } func TestActionClientList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } status := r.URL.Query()["status"] if len(status) != 2 { t.Errorf("expected status to contain 2 elements; got %q", status) } else { if status[0] != "running" { t.Errorf("expected status[0] to be running; got %q", status[0]) } if status[1] != "error" { t.Errorf("expected status[1] to be error; got %q", status[1]) } } sort := r.URL.Query()["sort"] if len(sort) != 3 { t.Errorf("expected sort to contain 3 elements; got %q", sort) } else { if sort[0] != "status" { t.Errorf("expected sort[0] to be status; got %q", sort[0]) } if sort[1] != "progress:desc" { t.Errorf("expected sort[1] to be progress:desc; got %q", sort[1]) } if sort[2] != "command:asc" { t.Errorf("expected sort[2] to be command:asc; got %q", sort[2]) } } _ = json.NewEncoder(w).Encode(schema.ActionListResponse{ Actions: []schema.Action{ {ID: 1}, {ID: 2}, }, }) }) opts := ActionListOpts{} opts.Page = 2 opts.PerPage = 50 opts.Status = []ActionStatus{ActionStatusRunning, ActionStatusError} opts.Sort = []string{"status", "progress:desc", "command:asc"} ctx := context.Background() actions, _, err := env.Client.Action.List(ctx, opts) if err != nil { t.Fatal(err) } if len(actions) != 2 { t.Fatal("expected 2 actions") } } func TestActionClientAll(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(struct { Actions []schema.Action `json:"actions"` Meta schema.Meta `json:"meta"` }{ Actions: []schema.Action{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() actions, err := env.Client.Action.All(ctx) if err != nil { t.Fatal(err) } if len(actions) != 3 { t.Fatalf("expected 3 actions; got %d", len(actions)) } if actions[0].ID != 1 || actions[1].ID != 2 || actions[2].ID != 3 { t.Errorf("unexpected actions") } } func TestActionClientWatchProgress(t *testing.T) { env := newTestEnv() defer env.Teardown() callCount := 0 env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { callCount++ w.Header().Set("Content-Type", "application/json") switch callCount { case 1: _ = json.NewEncoder(w).Encode(schema.ActionGetResponse{ Action: schema.Action{ ID: 1, Status: "running", Progress: 50, }, }) case 2: w.WriteHeader(http.StatusTooManyRequests) _ = json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeRateLimitExceeded), Message: "ratelimited", }, }) return case 3: _ = json.NewEncoder(w).Encode(schema.ActionGetResponse{ Action: schema.Action{ ID: 1, Status: "error", Progress: 100, Error: &schema.ActionError{ Code: "action_failed", Message: "action failed", }, }, }) default: t.Errorf("unexpected number of calls to the test server: %v", callCount) } }) action := &Action{ ID: 1, Status: ActionStatusRunning, Progress: 0, } ctx := context.Background() progressCh, errCh := env.Client.Action.WatchProgress(ctx, action) var ( progressUpdates []int err error ) loop: for { select { case progress := <-progressCh: progressUpdates = append(progressUpdates, progress) case err = <-errCh: break loop } } if err == nil { t.Fatal("expected an error") } if e, ok := err.(ActionError); !ok || e.Code != "action_failed" { t.Fatalf("expected hcloud.Error, but got: %#v", err) } if len(progressUpdates) != 1 || progressUpdates[0] != 50 { t.Fatalf("unexpected progress updates: %v", progressUpdates) } } func TestActionClientWatchProgressError(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) _ = json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeServiceError), Message: "service error", }, }) }) action := &Action{ID: 1} ctx := context.Background() _, errCh := env.Client.Action.WatchProgress(ctx, action) if err := <-errCh; err == nil { t.Fatal("expected an error") } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/certificate.go000066400000000000000000000253361414442033200250060ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // CertificateType is the type of available certificate types. type CertificateType string // Available certificate types. const ( CertificateTypeUploaded CertificateType = "uploaded" CertificateTypeManaged CertificateType = "managed" ) // CertificateStatusType is defines the type for the various managed // certificate status. type CertificateStatusType string // Possible certificate status. const ( CertificateStatusTypePending CertificateStatusType = "pending" CertificateStatusTypeFailed CertificateStatusType = "failed" // only in issuance CertificateStatusTypeCompleted CertificateStatusType = "completed" // only in renewal CertificateStatusTypeScheduled CertificateStatusType = "scheduled" CertificateStatusTypeUnavailable CertificateStatusType = "unavailable" ) // CertificateUsedByRefType is the type of used by references for // certificates. type CertificateUsedByRefType string // Possible users of certificates. const ( CertificateUsedByRefTypeLoadBalancer CertificateUsedByRefType = "load_balancer" ) // CertificateUsedByRef points to a resource that uses this certificate. type CertificateUsedByRef struct { ID int Type CertificateUsedByRefType } // CertificateStatus indicates the status of a managed certificate. type CertificateStatus struct { Issuance CertificateStatusType Renewal CertificateStatusType Error *Error } // IsFailed returns true if either the Issuance or the Renewal of a certificate // failed. In this case the FailureReason field details the nature of the // failure. func (st *CertificateStatus) IsFailed() bool { return st.Issuance == CertificateStatusTypeFailed || st.Renewal == CertificateStatusTypeFailed } // Certificate represents an certificate in the Hetzner Cloud. type Certificate struct { ID int Name string Labels map[string]string Type CertificateType Certificate string Created time.Time NotValidBefore time.Time NotValidAfter time.Time DomainNames []string Fingerprint string Status *CertificateStatus UsedBy []CertificateUsedByRef } // CertificateCreateResult is the result of creating a certificate. type CertificateCreateResult struct { Certificate *Certificate Action *Action } // 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 Type CertificateType Certificate string PrivateKey string Labels map[string]string DomainNames []string } // Validate checks if options are valid. func (o CertificateCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } switch o.Type { case "", CertificateTypeUploaded: return o.validateUploaded() case CertificateTypeManaged: return o.validateManaged() default: return fmt.Errorf("invalid type: %s", o.Type) } } func (o CertificateCreateOpts) validateManaged() error { if len(o.DomainNames) == 0 { return errors.New("no domain names") } return nil } func (o CertificateCreateOpts) validateUploaded() error { if o.Certificate == "" { return errors.New("missing certificate") } if o.PrivateKey == "" { return errors.New("missing private key") } return nil } // Create creates a new certificate uploaded certificate. // // Create returns an error for certificates of any other type. Use // CreateCertificate to create such certificates. func (c *CertificateClient) Create(ctx context.Context, opts CertificateCreateOpts) (*Certificate, *Response, error) { if !(opts.Type == "" || opts.Type == CertificateTypeUploaded) { return nil, nil, fmt.Errorf("invalid certificate type: %s", opts.Type) } result, resp, err := c.CreateCertificate(ctx, opts) if err != nil { return nil, resp, err } return result.Certificate, resp, nil } // CreateCertificate creates a new certificate of any type. func (c *CertificateClient) CreateCertificate( ctx context.Context, opts CertificateCreateOpts, ) (CertificateCreateResult, *Response, error) { var ( action *Action reqBody schema.CertificateCreateRequest ) if err := opts.Validate(); err != nil { return CertificateCreateResult{}, nil, err } reqBody.Name = opts.Name switch opts.Type { case "", CertificateTypeUploaded: reqBody.Type = string(CertificateTypeUploaded) reqBody.Certificate = opts.Certificate reqBody.PrivateKey = opts.PrivateKey case CertificateTypeManaged: reqBody.Type = string(CertificateTypeManaged) reqBody.DomainNames = opts.DomainNames default: return CertificateCreateResult{}, nil, fmt.Errorf("invalid certificate type: %v", opts.Type) } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return CertificateCreateResult{}, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/certificates", bytes.NewReader(reqBodyData)) if err != nil { return CertificateCreateResult{}, nil, err } respBody := schema.CertificateCreateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return CertificateCreateResult{}, resp, err } cert := CertificateFromSchema(respBody.Certificate) if respBody.Action != nil { action = ActionFromSchema(*respBody.Action) } return CertificateCreateResult{Certificate: cert, Action: action}, 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) } // RetryIssuance retries the issuance of a failed managed certificate. func (c *CertificateClient) RetryIssuance(ctx context.Context, certificate *Certificate) (*Action, *Response, error) { var respBody schema.CertificateIssuanceRetryResponse req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("/certificates/%d/actions/retry", certificate.ID), nil) if err != nil { return nil, nil, err } resp, err := c.client.Do(req, &respBody) if err != nil { return nil, nil, err } action := ActionFromSchema(respBody.Action) return action, resp, nil } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/certificate_test.go000066400000000000000000000255411414442033200260430ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestCertificateCreateOptsValidate_Uploaded(t *testing.T) { tests := []struct { name string opts CertificateCreateOpts errMsg string }{ { name: "missing name", opts: CertificateCreateOpts{ Certificate: "cert", PrivateKey: "key", Labels: map[string]string{}, }, errMsg: "missing name", }, { name: "no certificate", opts: CertificateCreateOpts{ Name: "name", PrivateKey: "key", Labels: map[string]string{}, }, errMsg: "missing certificate", }, { name: "no private key", opts: CertificateCreateOpts{ Name: "name", Certificate: "cert", Labels: map[string]string{}, }, errMsg: "missing private key", }, { name: "valid without type", opts: CertificateCreateOpts{ Name: "name", Certificate: "cert", PrivateKey: "key", Labels: map[string]string{}, }, }, { name: "valid with type", opts: CertificateCreateOpts{ Name: "name", Type: "uploaded", Certificate: "cert", PrivateKey: "key", Labels: map[string]string{}, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { err := tt.opts.Validate() if tt.errMsg != "" { assert.EqualError(t, err, tt.errMsg) return } assert.NoError(t, err) }) } } func TestCertificateCreateOptsValidate_Managed(t *testing.T) { tests := []struct { name string opts CertificateCreateOpts errMsg string }{ { name: "missing name", opts: CertificateCreateOpts{ Type: "managed", DomainNames: []string{"*.example.com", "example.com"}, }, errMsg: "missing name", }, { name: "missing domains", opts: CertificateCreateOpts{ Name: "I have no domains", Type: "managed", }, errMsg: "no domain names", }, { name: "valid certificate", opts: CertificateCreateOpts{ Name: "valid", Type: "managed", DomainNames: []string{"*.example.com", "example.com"}, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { err := tt.opts.Validate() if tt.errMsg != "" { assert.EqualError(t, err, tt.errMsg) return } assert.NoError(t, err) }) } } func TestCertificateCreateOptsValidate_InvalidType(t *testing.T) { opts := CertificateCreateOpts{Name: "invalid type", Type: "invalid"} err := opts.Validate() assert.EqualError(t, err, "invalid type: invalid") } func TestCertificateClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.CertificateGetResponse{ Certificate: schema.Certificate{ ID: 1, }, }) }) ctx := context.Background() certficate, _, err := env.Client.Certificate.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if certficate == nil { t.Fatal("no certficate") } if certficate.ID != 1 { t.Errorf("unexpected certficate ID: %v", certficate.ID) } t.Run("called via Get", func(t *testing.T) { certficate, _, err := env.Client.Certificate.Get(ctx, "1") if err != nil { t.Fatal(err) } if certficate == nil { t.Fatal("no certficate") } if certficate.ID != 1 { t.Errorf("unexpected certficate ID: %v", certficate.ID) } }) } func TestCertificateClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() certficate, _, err := env.Client.Certificate.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if certficate != nil { t.Fatal("expected no certficate") } } func TestCertificateClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mycert" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.CertificateListResponse{ Certificates: []schema.Certificate{ { ID: 1, Name: "mycert", }, }, }) }) ctx := context.Background() certficate, _, err := env.Client.Certificate.GetByName(ctx, "mycert") if err != nil { t.Fatal(err) } if certficate == nil { t.Fatal("no certficate") } if certficate.ID != 1 { t.Errorf("unexpected certficate ID: %v", certficate.ID) } t.Run("via Get", func(t *testing.T) { certficate, _, err := env.Client.Certificate.Get(ctx, "mycert") if err != nil { t.Fatal(err) } if certficate == nil { t.Fatal("no certficate") } if certficate.ID != 1 { t.Errorf("unexpected certficate ID: %v", certficate.ID) } }) } func TestCertificateClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mycert" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.CertificateListResponse{ Certificates: []schema.Certificate{}, }) }) ctx := context.Background() certficate, _, err := env.Client.Certificate.GetByName(ctx, "mycert") if err != nil { t.Fatal(err) } if certficate != nil { t.Fatal("unexpected certficate") } } func TestCertificateClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() certficate, _, err := env.Client.Certificate.GetByName(ctx, "") if err != nil { t.Fatal(err) } if certficate != nil { t.Fatal("unexpected certficate") } } func TestCertificateCreate_Uploaded(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.CertificateCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.CertificateCreateRequest{ Name: "my-cert", Type: "uploaded", Certificate: "-----BEGIN CERTIFICATE-----\n...", PrivateKey: "-----BEGIN PRIVATE KEY-----\n...", Labels: func() *map[string]string { labels := map[string]string{"key": "value"} return &labels }(), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.CertificateCreateResponse{ Certificate: schema.Certificate{ID: 1}, }) }) ctx := context.Background() opts := CertificateCreateOpts{ Name: "my-cert", Certificate: "-----BEGIN CERTIFICATE-----\n...", PrivateKey: "-----BEGIN PRIVATE KEY-----\n...", Labels: map[string]string{"key": "value"}, } _, _, err := env.Client.Certificate.Create(ctx, opts) if err != nil { t.Fatal(err) } } func TestCertificateCreate_Managed(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.CertificateCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.CertificateCreateRequest{ Name: "my-cert", Type: "managed", DomainNames: []string{"*.example.com", "example.com"}, Labels: func() *map[string]string { labels := map[string]string{"key": "value"} return &labels }(), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.CertificateCreateResponse{ Certificate: schema.Certificate{ID: 1}, Action: &schema.Action{ID: 14}, }) }) ctx := context.Background() opts := CertificateCreateOpts{ Name: "my-cert", Type: "managed", DomainNames: []string{"*.example.com", "example.com"}, Labels: map[string]string{"key": "value"}, } result, resp, err := env.Client.Certificate.CreateCertificate(ctx, opts) assert.NoError(t, err) assert.NotNil(t, resp, "no response returned") assert.NotNil(t, result.Certificate, "no certificate returned") assert.NotNil(t, result.Action, "no action returned") } func TestCertificateCreateValidation(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() opts := CertificateCreateOpts{} _, _, err := env.Client.Certificate.Create(ctx, opts) if err == nil || err.Error() != "missing name" { t.Fatalf("unexpected error: %v", err) } } func TestCertificateDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() certficate = &Certificate{ID: 1} ) _, err := env.Client.Certificate.Delete(ctx, certficate) if err != nil { t.Fatal(err) } } func TestCertificateClientUpdate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.CertificateUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.CertificateUpdateRequest{ Name: String("test"), Labels: func() *map[string]string { labels := map[string]string{"key": "value"} return &labels }(), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.CertificateUpdateResponse{ Certificate: schema.Certificate{ ID: 1, }, }) }) var ( ctx = context.Background() certficate = &Certificate{ID: 1} ) opts := CertificateUpdateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, } updatedCertificate, _, err := env.Client.Certificate.Update(ctx, certficate, opts) if err != nil { t.Fatal(err) } if updatedCertificate.ID != 1 { t.Errorf("unexpected certficate ID: %v", updatedCertificate.ID) } } func TestCertificateClient_RetryIssuance(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/certificates/1/actions/retry", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) resp := schema.CertificateIssuanceRetryResponse{ Action: schema.Action{ID: 1}, } err := json.NewEncoder(w).Encode(resp) assert.NoError(t, err) }) action, _, err := env.Client.Certificate.RetryIssuance(context.Background(), &Certificate{ID: 1}) assert.NoError(t, err) assert.Equal(t, &Action{ID: 1, Resources: []*ActionResource{}}, action) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/client.go000066400000000000000000000272671414442033200240070ustar00rootroot00000000000000package 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/internal/instrumentation" "github.com/hetznercloud/hcloud-go/hcloud/schema" "github.com/prometheus/client_golang/prometheus" ) // 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 instrumentationRegistry *prometheus.Registry Action ActionClient Certificate CertificateClient Datacenter DatacenterClient Firewall FirewallClient FloatingIP FloatingIPClient Image ImageClient ISO ISOClient LoadBalancer LoadBalancerClient LoadBalancerType LoadBalancerTypeClient Location LocationClient Network NetworkClient Pricing PricingClient Server ServerClient ServerType ServerTypeClient SSHKey SSHKeyClient Volume VolumeClient PlacementGroup PlacementGroupClient RDNS RDNSClient } // 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 } } // WithInstrumentation configures a Client to collect metrics about the performed HTTP requests. func WithInstrumentation(registry *prometheus.Registry) ClientOption { return func(client *Client) { client.instrumentationRegistry = registry } } // 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() if client.instrumentationRegistry != nil { i := instrumentation.New("api", client.instrumentationRegistry) client.httpClient.Transport = i.InstrumentedRoundTripper() } 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} client.Firewall = FirewallClient{client: client} client.PlacementGroup = PlacementGroupClient{client: client} client.RDNS = RDNSClient{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 { dumpReq, err := dumpRequest(r) 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)) error { var ( page = 1 ) for { resp, err := f(page) if err != nil { return err } if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 { return 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 dumpRequest(r *http.Request) ([]byte, error) { // Duplicate the request, so we can redact the auth header rDuplicate := r.Clone(context.Background()) rDuplicate.Header.Set("Authorization", "REDACTED") // To get the request body we need to read it before the request was actually sent. // See https://github.com/golang/go/issues/29792 dumpReq, err := httputil.DumpRequestOut(rDuplicate, true) if err != nil { return nil, err } // Set original request body to the duplicate created by DumpRequestOut. The request body is not duplicated // by .Clone() and instead just referenced, so it would be completely read otherwise. r.Body = rDuplicate.Body return dumpReq, nil } 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/client_test.go000066400000000000000000000207471414442033200250420ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) type testEnv struct { Server *httptest.Server Mux *http.ServeMux Client *Client } func (env *testEnv) Teardown() { env.Server.Close() env.Server = nil env.Mux = nil env.Client = nil } func newTestEnv() testEnv { mux := http.NewServeMux() server := httptest.NewServer(mux) client := NewClient( WithEndpoint(server.URL), WithToken("token"), WithBackoffFunc(func(_ int) time.Duration { return 0 }), ) return testEnv{ Server: server, Mux: mux, Client: client, } } func TestClientEndpointTrailingSlashesRemoved(t *testing.T) { client := NewClient(WithEndpoint("http://api/v1.0/////")) if strings.HasSuffix(client.endpoint, "/") { t.Fatalf("endpoint has trailing slashes: %q", client.endpoint) } } func TestClientError(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: "service_error", Message: "An error occurred", }, }) }) ctx := context.Background() req, err := env.Client.NewRequest(ctx, "GET", "/error", nil) if err != nil { t.Fatalf("error creating request: %s", err) } _, err = env.Client.Do(req, nil) if _, ok := err.(Error); !ok { t.Fatalf("unexpected error of type %T: %v", err, err) } apiError := err.(Error) if apiError.Code != "service_error" { t.Errorf("unexpected error code: %q", apiError.Code) } if apiError.Message != "An error occurred" { t.Errorf("unexpected error message: %q", apiError.Message) } } func TestClientMeta(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("RateLimit-Limit", "1000") w.Header().Set("RateLimit-Remaining", "999") w.Header().Set("RateLimit-Reset", "1511954577") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{ "foo": "bar", "meta": { "pagination": { "page": 1 } } }`) }) ctx := context.Background() req, err := env.Client.NewRequest(ctx, "GET", "/", nil) if err != nil { t.Fatalf("error creating request: %s", err) } response, err := env.Client.Do(req, nil) if err != nil { t.Fatalf("request failed: %s", err) } if response.Meta.Ratelimit.Limit != 1000 { t.Errorf("unexpected ratelimit limit: %d", response.Meta.Ratelimit.Limit) } if response.Meta.Ratelimit.Remaining != 999 { t.Errorf("unexpected ratelimit remaining: %d", response.Meta.Ratelimit.Remaining) } if !response.Meta.Ratelimit.Reset.Equal(time.Unix(1511954577, 0)) { t.Errorf("unexpected ratelimit reset: %v", response.Meta.Ratelimit.Reset) } if response.Meta.Pagination.Page != 1 { t.Error("missing pagination") } } func TestClientMetaNonJSON(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) fmt.Fprint(w, "foo") }) ctx := context.Background() req, err := env.Client.NewRequest(ctx, "GET", "/", nil) if err != nil { t.Fatalf("error creating request: %s", err) } response, err := env.Client.Do(req, nil) if err != nil { t.Fatalf("request failed: %s", err) } if response.Meta.Pagination != nil { t.Fatal("pagination should not be present") } } func TestClientAll(t *testing.T) { env := newTestEnv() defer env.Teardown() var ( ctx = context.Background() ratelimited bool expectedPage = 1 ) env.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") respBody := schema.MetaResponse{ Meta: schema.Meta{ Pagination: &schema.MetaPagination{ LastPage: 3, PerPage: 1, TotalEntries: 3, }, }, } switch page := r.URL.Query().Get("page"); page { case "", "1": respBody.Meta.Pagination.Page = 1 respBody.Meta.Pagination.NextPage = 2 case "2": if !ratelimited { ratelimited = true w.WriteHeader(http.StatusTooManyRequests) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeRateLimitExceeded), Message: "ratelimited", }, }) return } respBody.Meta.Pagination.Page = 2 respBody.Meta.Pagination.PreviousPage = 1 respBody.Meta.Pagination.NextPage = 3 case "3": respBody.Meta.Pagination.Page = 3 respBody.Meta.Pagination.PreviousPage = 2 default: t.Errorf("bad page: %q", page) } json.NewEncoder(w).Encode(respBody) }) _ = env.Client.all(func(page int) (*Response, error) { if page != expectedPage { t.Fatalf("expected page %d, but called for %d", expectedPage, page) } path := fmt.Sprintf("/?page=%d&per_page=1", page) req, err := env.Client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, err } resp, err := env.Client.Do(req, nil) if err != nil { return resp, err } expectedPage++ return resp, err }) if expectedPage != 4 { t.Errorf("expected to have walked through 3 pages, but walked through %d pages", expectedPage-1) } } func TestClientDo(t *testing.T) { env := newTestEnv() defer env.Teardown() callCount := 0 env.Mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { callCount++ w.Header().Set("Content-Type", "application/json") switch callCount { case 1: w.WriteHeader(http.StatusTooManyRequests) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeRateLimitExceeded), Message: "ratelimited", }, }) case 2: fmt.Fprintln(w, "{}") default: t.Errorf("unexpected number of calls to the test server: %v", callCount) } }) ctx := context.Background() request, _ := env.Client.NewRequest(ctx, http.MethodGet, "/test", nil) _, err := env.Client.Do(request, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if callCount != 2 { t.Fatalf("unexpected callCount: %v", callCount) } } func TestClientDoPost(t *testing.T) { env := newTestEnv() defer env.Teardown() debugLog := new(bytes.Buffer) env.Client.debugWriter = debugLog callCount := 0 env.Mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer token" { t.Errorf("unexpected auth header: %q, expected %q", auth, "Bearer token") } callCount++ w.Header().Set("Content-Type", "application/json") var dat map[string]interface{} body, err := ioutil.ReadAll(r.Body) r.Body.Close() if err != nil { t.Error(err) } if err := json.Unmarshal(body, &dat); err != nil { t.Error(err) } switch callCount { case 1: if dat["test"] != "abcd" { t.Errorf("unexpected payload: %v", dat) } w.WriteHeader(http.StatusConflict) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeConflict), Message: "conflict", }, }) case 2: if dat["test"] != "abcd" { t.Errorf("unexpected payload: %v", dat) } fmt.Fprintln(w, "{}") default: t.Errorf("unexpected number of calls to the test server: %v", callCount) } }) ctx := context.Background() request, _ := env.Client.NewRequest(ctx, http.MethodPost, "/test", strings.NewReader(`{"test": "abcd"}`)) _, err := env.Client.Do(request, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if callCount != 2 { t.Fatalf("unexpected callCount: %v", callCount) } if strings.Contains(debugLog.String(), "token") { t.Errorf("debug log did contain token, although it shouldn't") } } func TestBuildUserAgent(t *testing.T) { testCases := []struct { name string applicationName string applicationVersion string userAgent string }{ {"with application name and version", "test", "1.0", "test/1.0 " + UserAgent}, {"with application name but no version", "test", "", "test " + UserAgent}, {"without application name and version", "", "", UserAgent}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { client := NewClient(WithApplication(testCase.applicationName, testCase.applicationVersion)) if client.userAgent != testCase.userAgent { t.Errorf("unexpected user agent: %v", client.userAgent) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/datacenter.go000066400000000000000000000070121414442033200246250ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/datacenter_test.go000066400000000000000000000126201414442033200256650ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestDatacenterClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.DatacenterGetResponse{ Datacenter: schema.Datacenter{ ID: 1, }, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } t.Run("via Get", func(t *testing.T) { datacenter, _, err := env.Client.Datacenter.Get(ctx, "1") if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if datacenter != nil { t.Fatal("expected no datacenter") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.DatacenterListResponse{ Datacenters: []schema.Datacenter{ { ID: 1, }, }, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } t.Run("via Get", func(t *testing.T) { datacenter, _, err := env.Client.Datacenter.Get(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.DatacenterListResponse{ Datacenters: []schema.Datacenter{}, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if datacenter != nil { t.Fatal("unexpected datacenter") } }) t.Run("GetByName (empty)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "") if err != nil { t.Fatal(err) } if datacenter != nil { t.Fatal("unexpected datacenter") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } if name := r.URL.Query().Get("name"); name != "nbg1-dc3" { t.Errorf("expected name nbg1-dc3; got %q", name) } json.NewEncoder(w).Encode(schema.DatacenterListResponse{ Datacenters: []schema.Datacenter{ {ID: 1}, {ID: 2}, }, }) }) opts := DatacenterListOpts{} opts.Page = 2 opts.PerPage = 50 opts.Name = "nbg1-dc3" ctx := context.Background() datacenters, _, err := env.Client.Datacenter.List(ctx, opts) if err != nil { t.Fatal(err) } if len(datacenters) != 2 { t.Fatal("expected 2 datacenters") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Datacenters []schema.Datacenter `json:"datacenters"` Meta schema.Meta `json:"meta"` }{ Datacenters: []schema.Datacenter{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() datacenters, err := env.Client.Datacenter.All(ctx) if err != nil { t.Fatalf("Datacenter.List failed: %s", err) } if len(datacenters) != 3 { t.Fatalf("expected 3 datacenters; got %d", len(datacenters)) } if datacenters[0].ID != 1 || datacenters[1].ID != 2 || datacenters[2].ID != 3 { t.Errorf("unexpected datacenters") } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/error.go000066400000000000000000000201561414442033200236500ustar00rootroot00000000000000package hcloud import ( "fmt" "net" ) // 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 ErrorCodeRobotUnavailable ErrorCode = "robot_unavailable" // Robot was not available. The caller may retry the operation after a short delay ErrorUnsupportedError ErrorCode = "unsupported_error" // The gives resource does not support this // Server related error codes ErrorCodeInvalidServerType ErrorCode = "invalid_server_type" // The server type does not fit for the given server or is deprecated ErrorCodeServerNotStopped ErrorCode = "server_not_stopped" // The action requires a stopped server ErrorCodeNetworksOverlap ErrorCode = "networks_overlap" // The network IP range overlaps with one of the server networks ErrorCodePlacementError ErrorCode = "placement_error" // An error during the placement occurred ErrorCodeServerAlreadyAttached ErrorCode = "server_already_attached" // The server is already attached to the resource // Load Balancer related error codes ErrorCodeIPNotOwned ErrorCode = "ip_not_owned" // The IP you are trying to add as a target is not owned by the Project owner ErrorCodeSourcePortAlreadyUsed ErrorCode = "source_port_already_used" // The source port you are trying to add is already in use ErrorCodeCloudResourceIPNotAllowed ErrorCode = "cloud_resource_ip_not_allowed" // The IP you are trying to add as a target belongs to a Hetzner Cloud resource ErrorCodeServerNotAttachedToNetwork ErrorCode = "server_not_attached_to_network" // The server you are trying to add as a target is not attached to the same network as the Load Balancer ErrorCodeTargetAlreadyDefined ErrorCode = "target_already_defined" // The Load Balancer target you are trying to define is already defined ErrorCodeInvalidLoadBalancerType ErrorCode = "invalid_load_balancer_type" // The Load Balancer type does not fit for the given Load Balancer ErrorCodeLoadBalancerAlreadyAttached ErrorCode = "load_balancer_already_attached" // The Load Balancer is already attached to a network ErrorCodeTargetsWithoutUsePrivateIP ErrorCode = "targets_without_use_private_ip" // The Load Balancer has targets that use the public IP instead of the private IP ErrorCodeLoadBalancerNotAttachedToNetwork ErrorCode = "load_balancer_not_attached_to_network" // The Load Balancer is not attached to a network // Network related error codes ErrorCodeIPNotAvailable ErrorCode = "ip_not_available" // The provided Network IP is not available ErrorCodeNoSubnetAvailable ErrorCode = "no_subnet_available" // No Subnet or IP is available for the Load Balancer/Server within the network ErrorCodeVSwitchAlreadyUsed ErrorCode = "vswitch_id_already_used" // The given Robot vSwitch ID is already registered in another network // Volume related error codes ErrorCodeNoSpaceLeftInLocation ErrorCode = "no_space_left_in_location" // There is no volume space left in the given location ErrorCodeVolumeAlreadyAttached ErrorCode = "volume_already_attached" // Volume is already attached to a server, detach first // Firewall related error codes ErrorCodeFirewallAlreadyApplied ErrorCode = "firewall_already_applied" // Firewall was already applied on resource ErrorCodeFirewallAlreadyRemoved ErrorCode = "firewall_already_removed" // Firewall was already removed from the resource ErrorCodeIncompatibleNetworkType ErrorCode = "incompatible_network_type" // The Network type is incompatible for the given resource ErrorCodeResourceInUse ErrorCode = "resource_in_use" // Firewall must not be in use to be deleted ErrorCodeServerAlreadyAdded ErrorCode = "server_already_added" // Server added more than one time to resource ErrorCodeFirewallResourceNotFound ErrorCode = "firewall_resource_not_found" // Resource a firewall should be attached to / detached from not found // Certificate related error codes ErrorCodeCAARecordDoesNotAllowCA ErrorCode = "caa_record_does_not_allow_ca" // CAA record does not allow certificate authority ErrorCodeCADNSValidationFailed ErrorCode = "ca_dns_validation_failed" // Certificate Authority: DNS validation failed ErrorCodeCATooManyAuthorizationsFailedRecently ErrorCode = "ca_too_many_authorizations_failed_recently" // Certificate Authority: Too many authorizations failed recently ErrorCodeCATooManyCertificatedIssuedForRegisteredDomain ErrorCode = "ca_too_many_certificates_issued_for_registered_domain" // Certificate Authority: Too many certificates issued for registered domain ErrorCodeCATooManyDuplicateCertificates ErrorCode = "ca_too_many_duplicate_certificates" // Certificate Authority: Too many duplicate certificates ErrorCodeCloudNotVerifyDomainDelegatedToZone ErrorCode = "could_not_verify_domain_delegated_to_zone" // Could not verify domain delegated to zone ErrorCodeDNSZoneNotFound ErrorCode = "dns_zone_not_found" // DNS zone not found // 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 } type InvalidIPError struct { IP string } func (e InvalidIPError) Error() string { return fmt.Sprintf("could not parse ip address %s", e.IP) } type DNSNotFoundError struct { IP net.IP } func (e DNSNotFoundError) Error() string { return fmt.Sprintf("dns for ip %s not found", e.IP.String()) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/firewall.go000066400000000000000000000255521414442033200243310ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Firewall represents a Firewall in the Hetzner Cloud. type Firewall struct { ID int Name string Labels map[string]string Created time.Time Rules []FirewallRule AppliedTo []FirewallResource } // FirewallRule represents a Firewall's rules. type FirewallRule struct { Direction FirewallRuleDirection SourceIPs []net.IPNet DestinationIPs []net.IPNet Protocol FirewallRuleProtocol Port *string Description *string } // FirewallRuleDirection specifies the direction of a Firewall rule. type FirewallRuleDirection string const ( // FirewallRuleDirectionIn specifies a rule for inbound traffic. FirewallRuleDirectionIn FirewallRuleDirection = "in" // FirewallRuleDirectionOut specifies a rule for outbound traffic. FirewallRuleDirectionOut FirewallRuleDirection = "out" ) // FirewallRuleProtocol specifies the protocol of a Firewall rule. type FirewallRuleProtocol string const ( // FirewallRuleProtocolTCP specifies a TCP rule. FirewallRuleProtocolTCP FirewallRuleProtocol = "tcp" // FirewallRuleProtocolUDP specifies a UDP rule. FirewallRuleProtocolUDP FirewallRuleProtocol = "udp" // FirewallRuleProtocolICMP specifies an ICMP rule. FirewallRuleProtocolICMP FirewallRuleProtocol = "icmp" // FirewallRuleProtocolESP specifies an esp rule. FirewallRuleProtocolESP FirewallRuleProtocol = "esp" // FirewallRuleProtocolGRE specifies an gre rule. FirewallRuleProtocolGRE FirewallRuleProtocol = "gre" ) // FirewallResourceType specifies the resource to apply a Firewall on. type FirewallResourceType string const ( // FirewallResourceTypeServer specifies a Server. FirewallResourceTypeServer FirewallResourceType = "server" // FirewallResourceTypeLabelSelector specifies a LabelSelector. FirewallResourceTypeLabelSelector FirewallResourceType = "label_selector" ) // FirewallResource represents a resource to apply the new Firewall on. type FirewallResource struct { Type FirewallResourceType Server *FirewallResourceServer LabelSelector *FirewallResourceLabelSelector } // FirewallResourceServer represents a Server to apply a Firewall on. type FirewallResourceServer struct { ID int } // FirewallResourceLabelSelector represents a LabelSelector to apply a Firewall on. type FirewallResourceLabelSelector struct { Selector string } // FirewallClient is a client for the Firewalls API. type FirewallClient struct { client *Client } // GetByID retrieves a Firewall by its ID. If the Firewall does not exist, nil is returned. func (c *FirewallClient) GetByID(ctx context.Context, id int) (*Firewall, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/firewalls/%d", id), nil) if err != nil { return nil, nil, err } var body schema.FirewallGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return FirewallFromSchema(body.Firewall), resp, nil } // GetByName retrieves a Firewall by its name. If the Firewall does not exist, nil is returned. func (c *FirewallClient) GetByName(ctx context.Context, name string) (*Firewall, *Response, error) { if name == "" { return nil, nil, nil } firewalls, response, err := c.List(ctx, FirewallListOpts{Name: name}) if len(firewalls) == 0 { return nil, response, err } return firewalls[0], response, err } // Get retrieves a Firewall by its ID if the input can be parsed as an integer, otherwise it // retrieves a Firewall by its name. If the Firewall does not exist, nil is returned. func (c *FirewallClient) Get(ctx context.Context, idOrName string) (*Firewall, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // FirewallListOpts specifies options for listing Firewalls. type FirewallListOpts struct { ListOpts Name string } func (l FirewallListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } return vals } // List returns a list of Firewalls 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 *FirewallClient) List(ctx context.Context, opts FirewallListOpts) ([]*Firewall, *Response, error) { path := "/firewalls?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.FirewallListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } firewalls := make([]*Firewall, 0, len(body.Firewalls)) for _, s := range body.Firewalls { firewalls = append(firewalls, FirewallFromSchema(s)) } return firewalls, resp, nil } // All returns all Firewalls. func (c *FirewallClient) All(ctx context.Context) ([]*Firewall, error) { allFirewalls := []*Firewall{} opts := FirewallListOpts{} opts.PerPage = 50 err := c.client.all(func(page int) (*Response, error) { opts.Page = page firewalls, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allFirewalls = append(allFirewalls, firewalls...) return resp, nil }) if err != nil { return nil, err } return allFirewalls, nil } // AllWithOpts returns all Firewalls for the given options. func (c *FirewallClient) AllWithOpts(ctx context.Context, opts FirewallListOpts) ([]*Firewall, error) { var allFirewalls []*Firewall err := c.client.all(func(page int) (*Response, error) { opts.Page = page firewalls, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allFirewalls = append(allFirewalls, firewalls...) return resp, nil }) if err != nil { return nil, err } return allFirewalls, nil } // FirewallCreateOpts specifies options for creating a new Firewall. type FirewallCreateOpts struct { Name string Labels map[string]string Rules []FirewallRule ApplyTo []FirewallResource } // Validate checks if options are valid. func (o FirewallCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } return nil } // FirewallCreateResult is the result of a create Firewall call. type FirewallCreateResult struct { Firewall *Firewall Actions []*Action } // Create creates a new Firewall. func (c *FirewallClient) Create(ctx context.Context, opts FirewallCreateOpts) (FirewallCreateResult, *Response, error) { if err := opts.Validate(); err != nil { return FirewallCreateResult{}, nil, err } reqBody := firewallCreateOptsToSchema(opts) reqBodyData, err := json.Marshal(reqBody) if err != nil { return FirewallCreateResult{}, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/firewalls", bytes.NewReader(reqBodyData)) if err != nil { return FirewallCreateResult{}, nil, err } respBody := schema.FirewallCreateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return FirewallCreateResult{}, resp, err } result := FirewallCreateResult{ Firewall: FirewallFromSchema(respBody.Firewall), Actions: ActionsFromSchema(respBody.Actions), } return result, resp, nil } // FirewallUpdateOpts specifies options for updating a Firewall. type FirewallUpdateOpts struct { Name string Labels map[string]string } // Update updates a Firewall. func (c *FirewallClient) Update(ctx context.Context, firewall *Firewall, opts FirewallUpdateOpts) (*Firewall, *Response, error) { reqBody := schema.FirewallUpdateRequest{} 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("/firewalls/%d", firewall.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.FirewallUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return FirewallFromSchema(respBody.Firewall), resp, nil } // Delete deletes a Firewall. func (c *FirewallClient) Delete(ctx context.Context, firewall *Firewall) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/firewalls/%d", firewall.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } // FirewallSetRulesOpts specifies options for setting rules of a Firewall. type FirewallSetRulesOpts struct { Rules []FirewallRule } // SetRules sets the rules of a Firewall. func (c *FirewallClient) SetRules(ctx context.Context, firewall *Firewall, opts FirewallSetRulesOpts) ([]*Action, *Response, error) { reqBody := firewallSetRulesOptsToSchema(opts) reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/firewalls/%d/actions/set_rules", firewall.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.FirewallActionSetRulesResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionsFromSchema(respBody.Actions), resp, nil } func (c *FirewallClient) ApplyResources(ctx context.Context, firewall *Firewall, resources []FirewallResource) ([]*Action, *Response, error) { applyTo := make([]schema.FirewallResource, len(resources)) for i, r := range resources { applyTo[i] = firewallResourceToSchema(r) } reqBody := schema.FirewallActionApplyToResourcesRequest{ApplyTo: applyTo} reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/firewalls/%d/actions/apply_to_resources", firewall.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.FirewallActionApplyToResourcesResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionsFromSchema(respBody.Actions), resp, nil } func (c *FirewallClient) RemoveResources(ctx context.Context, firewall *Firewall, resources []FirewallResource) ([]*Action, *Response, error) { removeFrom := make([]schema.FirewallResource, len(resources)) for i, r := range resources { removeFrom[i] = firewallResourceToSchema(r) } reqBody := schema.FirewallActionRemoveFromResourcesRequest{RemoveFrom: removeFrom} reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/firewalls/%d/actions/remove_from_resources", firewall.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.FirewallActionRemoveFromResourcesResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionsFromSchema(respBody.Actions), resp, nil } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/firewall_test.go000066400000000000000000000325151414442033200253650ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net" "net/http" "testing" "github.com/google/go-cmp/cmp" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestFirewallCreateOptsValidate(t *testing.T) { testCases := map[string]struct { Opts FirewallCreateOpts Valid bool }{ "empty": { Opts: FirewallCreateOpts{}, Valid: false, }, "all set": { Opts: FirewallCreateOpts{ Name: "name", Labels: map[string]string{}, }, Valid: true, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { err := testCase.Opts.Validate() if err == nil && !testCase.Valid || err != nil && testCase.Valid { t.FailNow() } }) } } func TestFirewallClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.FirewallGetResponse{ Firewall: schema.Firewall{ ID: 1, }, }) }) ctx := context.Background() firewall, _, err := env.Client.Firewall.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if firewall == nil { t.Fatal("no firewall") } if firewall.ID != 1 { t.Errorf("unexpected firewall ID: %v", firewall.ID) } t.Run("called via Get", func(t *testing.T) { firewall, _, err := env.Client.Firewall.Get(ctx, "1") if err != nil { t.Fatal(err) } if firewall == nil { t.Fatal("no firewall") } if firewall.ID != 1 { t.Errorf("unexpected firewall ID: %v", firewall.ID) } }) } func TestFirewallClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() firewall, _, err := env.Client.Firewall.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if firewall != nil { t.Fatal("expected no firewall") } } func TestFirewallClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myfirewall" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.FirewallListResponse{ Firewalls: []schema.Firewall{ { ID: 1, Name: "myfirewall", }, }, }) }) ctx := context.Background() firewall, _, err := env.Client.Firewall.GetByName(ctx, "myfirewall") if err != nil { t.Fatal(err) } if firewall == nil { t.Fatal("no firewall") } if firewall.ID != 1 { t.Errorf("unexpected firewall ID: %v", firewall.ID) } t.Run("via Get", func(t *testing.T) { firewall, _, err := env.Client.Firewall.Get(ctx, "myfirewall") if err != nil { t.Fatal(err) } if firewall == nil { t.Fatal("no firewall") } if firewall.ID != 1 { t.Errorf("unexpected firewall ID: %v", firewall.ID) } }) } func TestFirewallClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myfirewall" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.FirewallListResponse{ Firewalls: []schema.Firewall{}, }) }) ctx := context.Background() firewall, _, err := env.Client.Firewall.GetByName(ctx, "myfirewall") if err != nil { t.Fatal(err) } if firewall != nil { t.Fatal("unexpected firewall") } } func TestFirewallClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() firewall, _, err := env.Client.Firewall.GetByName(ctx, "") if err != nil { t.Fatal(err) } if firewall != nil { t.Fatal("unexpected firewall") } } func TestFirewallCreate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.FirewallCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.FirewallCreateRequest{ Name: "myfirewall", Labels: func() *map[string]string { labels := map[string]string{"key": "value"} return &labels }(), ApplyTo: []schema.FirewallResource{ { Type: "server", Server: &schema.FirewallResourceServer{ ID: 2, }, }, }, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.FirewallCreateResponse{ Firewall: schema.Firewall{ID: 1}, }) }) ctx := context.Background() opts := FirewallCreateOpts{ Name: "myfirewall", Labels: map[string]string{"key": "value"}, ApplyTo: []FirewallResource{ { Type: FirewallResourceTypeServer, Server: &FirewallResourceServer{ ID: 2, }, }, }, } _, _, err := env.Client.Firewall.Create(ctx, opts) if err != nil { t.Fatal(err) } } func TestFirewallCreateValidation(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() opts := FirewallCreateOpts{} _, _, err := env.Client.Firewall.Create(ctx, opts) if err == nil || err.Error() != "missing name" { t.Fatalf("unexpected error: %v", err) } } func TestFirewallDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() firewall = &Firewall{ID: 1} ) _, err := env.Client.Firewall.Delete(ctx, firewall) if err != nil { t.Fatal(err) } } func TestFirewallClientUpdate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FirewallUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.FirewallUpdateRequest{ Name: String("test"), Labels: func() *map[string]string { labels := map[string]string{"key": "value"} return &labels }(), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.FirewallUpdateResponse{ Firewall: schema.Firewall{ ID: 1, }, }) }) var ( ctx = context.Background() firewall = &Firewall{ID: 1} ) opts := FirewallUpdateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, } updatedFirewall, _, err := env.Client.Firewall.Update(ctx, firewall, opts) if err != nil { t.Fatal(err) } if updatedFirewall.ID != 1 { t.Errorf("unexpected firewall ID: %v", updatedFirewall.ID) } } func TestFirewallSetRules(t *testing.T) { description := "allow icmp out" tests := []struct { name string expectedReqBody schema.FirewallActionSetRulesRequest opts FirewallSetRulesOpts }{ { name: "direction in", expectedReqBody: schema.FirewallActionSetRulesRequest{ Rules: []schema.FirewallRule{ { Direction: "in", SourceIPs: []string{"10.0.0.5/32", "10.0.0.6/32"}, Protocol: "icmp", }, }, }, opts: FirewallSetRulesOpts{ Rules: []FirewallRule{ { Direction: FirewallRuleDirectionIn, SourceIPs: []net.IPNet{ { IP: net.ParseIP("10.0.0.5"), Mask: net.CIDRMask(32, 32), }, { IP: net.ParseIP("10.0.0.6"), Mask: net.CIDRMask(32, 32), }, }, Protocol: FirewallRuleProtocolICMP, }, }, }, }, { name: "direction out", expectedReqBody: schema.FirewallActionSetRulesRequest{ Rules: []schema.FirewallRule{ { Direction: "out", DestinationIPs: []string{"10.0.0.5/32", "10.0.0.6/32"}, Protocol: "icmp", }, }, }, opts: FirewallSetRulesOpts{ Rules: []FirewallRule{ { Direction: FirewallRuleDirectionOut, DestinationIPs: []net.IPNet{ { IP: net.ParseIP("10.0.0.5"), Mask: net.CIDRMask(32, 32), }, { IP: net.ParseIP("10.0.0.6"), Mask: net.CIDRMask(32, 32), }, }, Protocol: FirewallRuleProtocolICMP, }, }, }, }, { name: "empty", expectedReqBody: schema.FirewallActionSetRulesRequest{ Rules: []schema.FirewallRule{}, }, opts: FirewallSetRulesOpts{ Rules: []FirewallRule{}, }, }, { name: "description", expectedReqBody: schema.FirewallActionSetRulesRequest{ Rules: []schema.FirewallRule{ { Direction: "out", DestinationIPs: []string{"10.0.0.5/32", "10.0.0.6/32"}, Protocol: "icmp", Description: &description, }, }, }, opts: FirewallSetRulesOpts{ Rules: []FirewallRule{ { Direction: FirewallRuleDirectionOut, DestinationIPs: []net.IPNet{ { IP: net.ParseIP("10.0.0.5"), Mask: net.CIDRMask(32, 32), }, { IP: net.ParseIP("10.0.0.6"), Mask: net.CIDRMask(32, 32), }, }, Protocol: FirewallRuleProtocolICMP, Description: &description, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls/1/actions/set_rules", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FirewallActionSetRulesRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if !cmp.Equal(tt.expectedReqBody, reqBody) { t.Log(cmp.Diff(tt.expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.FirewallActionSetRulesResponse{ Actions: []schema.Action{ { ID: 1, }, }, }) }) var ( ctx = context.Background() firewall = &Firewall{ID: 1} ) actions, _, err := env.Client.Firewall.SetRules(ctx, firewall, tt.opts) if err != nil { t.Fatal(err) } if len(actions) != 1 || actions[0].ID != 1 { t.Errorf("unexpected actions: %v", actions) } }) } } func TestFirewallApplyToResources(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls/1/actions/apply_to_resources", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FirewallActionApplyToResourcesRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.FirewallActionApplyToResourcesRequest{ ApplyTo: []schema.FirewallResource{ { Type: "server", Server: &schema.FirewallResourceServer{ID: 5}, }, { Type: "label_selector", LabelSelector: &schema.FirewallResourceLabelSelector{Selector: "a=b"}, }, }, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.FirewallActionApplyToResourcesResponse{ Actions: []schema.Action{ { ID: 1, }, }, }) }) var ( ctx = context.Background() firewall = &Firewall{ID: 1} ) resources := []FirewallResource{ { Type: FirewallResourceTypeServer, Server: &FirewallResourceServer{ID: 5}, }, { Type: FirewallResourceTypeLabelSelector, LabelSelector: &FirewallResourceLabelSelector{Selector: "a=b"}, }, } actions, _, err := env.Client.Firewall.ApplyResources(ctx, firewall, resources) if err != nil { t.Fatal(err) } if len(actions) != 1 || actions[0].ID != 1 { t.Errorf("unexpected actions: %v", actions) } } func TestFirewallRemoveFromResources(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/firewalls/1/actions/remove_from_resources", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FirewallActionRemoveFromResourcesRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.FirewallActionRemoveFromResourcesRequest{ RemoveFrom: []schema.FirewallResource{ { Type: "server", Server: &schema.FirewallResourceServer{ID: 5}, }, { Type: "label_selector", LabelSelector: &schema.FirewallResourceLabelSelector{Selector: "a=b"}, }, }, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.FirewallActionRemoveFromResourcesResponse{ Actions: []schema.Action{ { ID: 1, }, }, }) }) var ( ctx = context.Background() firewall = &Firewall{ID: 1} ) resources := []FirewallResource{ { Type: FirewallResourceTypeServer, Server: &FirewallResourceServer{ID: 5}, }, { Type: FirewallResourceTypeLabelSelector, LabelSelector: &FirewallResourceLabelSelector{Selector: "a=b"}, }, } actions, _, err := env.Client.Firewall.RemoveResources(ctx, firewall, resources) if err != nil { t.Fatal(err) } if len(actions) != 1 || actions[0].ID != 1 { t.Errorf("unexpected actions: %v", actions) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/floating_ip.go000066400000000000000000000267671414442033200250300ustar00rootroot00000000000000package 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. // Deprecated: Use GetDNSPtrForIP instead 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" ) // changeDNSPtr changes or resets the reverse DNS pointer for a IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (f *FloatingIP) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) { reqBody := schema.FloatingIPActionChangeDNSPtrRequest{ IP: ip.String(), DNSPtr: ptr, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", f.ID) req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.FloatingIPActionChangeDNSPtrResponse{} resp, err := client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // GetDNSPtrForIP searches for the dns assigned to the given IP address. // It returns an error if there is no dns set for the given IP address. func (f *FloatingIP) GetDNSPtrForIP(ip net.IP) (string, error) { dns, ok := f.DNSPtr[ip.String()] if !ok { return "", DNSNotFoundError{ip} } return dns, nil } // 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) { netIP := net.ParseIP(ip) if netIP == nil { return nil, nil, InvalidIPError{ip} } return floatingIP.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr) } // 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/floating_ip_test.go000066400000000000000000000344431414442033200260550ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestFloatingIPClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.FloatingIPGetResponse{ FloatingIP: schema.FloatingIP{ ID: 1, Type: "ipv4", IP: "131.232.99.1", }, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected ID: %v", floatingIP.ID) } t.Run("via Get", func(t *testing.T) { floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "1") if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", floatingIP.ID) } }) } func TestFloatingIPClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if floatingIP != nil { t.Fatal("expected no Floating IP") } } func TestFloatingIPClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myFloatingIP" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{ { ID: 1, Name: "myFloatingIP", Type: "ipv4", IP: "131.232.99.1", }, }, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByName(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected ID: %v", floatingIP.ID) } t.Run("via Get", func(t *testing.T) { floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", floatingIP.ID) } }) } func TestFloatingIPClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myFloatingIP" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{}, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByName(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP != nil { t.Fatal("expected no Floating IP") } t.Run("via Get", func(t *testing.T) { floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP != nil { t.Fatal("expected no Floating IP") } }) } func TestFloatingIPClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() floatingIP, _, err := env.Client.FloatingIP.GetByName(context.Background(), "") if err != nil { t.Fatal(err) } if floatingIP != nil { t.Fatal("unexpected Floating IP") } } func TestFloatingIPClientList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{ {ID: 1, Type: "ipv4", IP: "131.232.99.1"}, {ID: 2, Type: "ipv4", IP: "131.232.99.1"}, }, }) }) opts := FloatingIPListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() floatingIPs, _, err := env.Client.FloatingIP.List(ctx, opts) if err != nil { t.Fatal(err) } if len(floatingIPs) != 2 { t.Fatal("expected 2 Floating IPs") } } func TestFloatingIPClientAllWithOpts(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{ {ID: 1, Type: "ipv4", IP: "131.232.99.1"}, {ID: 2, Type: "ipv4", IP: "131.232.99.1"}, }, }) }) ctx := context.Background() opts := FloatingIPListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}} floatingIPs, err := env.Client.FloatingIP.AllWithOpts(ctx, opts) if err != nil { t.Fatal(err) } if len(floatingIPs) != 2 { t.Fatal("expected 2 Floating IPs") } } func TestFloatingIPClientCreate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FloatingIPCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.FloatingIPCreateResponse{ FloatingIP: schema.FloatingIP{ID: 1, Type: "ipv4", IP: "131.232.99.1"}, Action: &schema.Action{ ID: 1, }, }) }) opts := FloatingIPCreateOpts{ Type: FloatingIPTypeIPv4, Description: String("test"), HomeLocation: &Location{Name: "test"}, Server: &Server{ID: 1}, Labels: map[string]string{"key": "value"}, } ctx := context.Background() result, _, err := env.Client.FloatingIP.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.FloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %d", result.FloatingIP.ID) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } } func TestFloatingIPClientCreateWithName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FloatingIPCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name == nil || *reqBody.Name != "MyFloatingIP" { t.Errorf("unexpected name in request: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.FloatingIPCreateResponse{ FloatingIP: schema.FloatingIP{ID: 1, Type: "ipv4", IP: "131.232.99.1"}, Action: &schema.Action{ ID: 1, }, }) }) opts := FloatingIPCreateOpts{ Type: FloatingIPTypeIPv4, Description: String("test"), HomeLocation: &Location{Name: "test"}, Server: &Server{ID: 1}, Name: String("MyFloatingIP"), Labels: map[string]string{"key": "value"}, } ctx := context.Background() result, _, err := env.Client.FloatingIP.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.FloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %d", result.FloatingIP.ID) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } } func TestFloatingIPClientDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) _, err := env.Client.FloatingIP.Delete(ctx, floatingIP) if err != nil { t.Fatal(err) } } func TestFloatingIPClientUpdate(t *testing.T) { var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) t.Run("update description", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description != "test" { t.Errorf("unexpected description: %v", reqBody.Description) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{ Description: "test", } updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{ Labels: map[string]string{"key": "value"}, } updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{ Name: "test", } updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description != "" { t.Errorf("unexpected no description, but got: %v", reqBody.Description) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{} updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) } func TestFloatingIPClientAssign(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/assign", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.FloatingIPActionAssignRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Server != 1 { t.Errorf("unexpected server ID: %d", reqBody.Server) } json.NewEncoder(w).Encode(schema.FloatingIPActionAssignResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} server = &Server{ID: 1} ) action, _, err := env.Client.FloatingIP.Assign(ctx, floatingIP, server) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestFloatingIPClientUnassign(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/unassign", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.FloatingIPActionAssignResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) action, _, err := env.Client.FloatingIP.Unassign(ctx, floatingIP) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestFloatingIPClientChangeProtection(t *testing.T) { var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) t.Run("enable delete protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FloatingIPActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } json.NewEncoder(w).Encode(schema.FloatingIPActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := FloatingIPChangeProtectionOpts{ Delete: Bool(true), } action, _, err := env.Client.FloatingIP.ChangeProtection(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/hcloud.go000066400000000000000000000002461414442033200237730ustar00rootroot00000000000000// Package hcloud is a library for the Hetzner Cloud API. package hcloud // Version is the library's version following Semantic Versioning. const Version = "1.32.0" golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/hcloud_test.go000066400000000000000000000006651414442033200250370ustar00rootroot00000000000000package hcloud_test import ( "context" "fmt" "log" "github.com/hetznercloud/hcloud-go/hcloud" ) func Example() { client := hcloud.NewClient(hcloud.WithToken("token")) server, _, err := client.Server.GetByID(context.Background(), 1) if err != nil { log.Fatalf("error retrieving server: %s\n", err) } if server != nil { fmt.Printf("server 1 is called %q\n", server.Name) } else { fmt.Println("server 1 not found") } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/helper.go000066400000000000000000000010251414442033200237700ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/image.go000066400000000000000000000164561414442033200236110ustar00rootroot00000000000000package 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 Deleted time.Time } // IsDeprecated returns whether the image is deprecated. func (image *Image) IsDeprecated() bool { return !image.Deprecated.IsZero() } // IsDeleted returns whether the image is deleted. func (image *Image) IsDeleted() bool { return !image.Deleted.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" // ImageTypeApp represents a one click app image. ImageTypeApp ImageType = "app" ) // 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/image_test.go000066400000000000000000000237011414442033200246370ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestImageIsDeprecated(t *testing.T) { t.Run("not deprecated", func(t *testing.T) { image := &Image{} if image.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", image.IsDeprecated()) } }) t.Run("deprecated", func(t *testing.T) { image := &Image{ Deprecated: time.Now(), } if !image.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", image.IsDeprecated()) } }) } func TestImageClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ImageGetResponse{ Image: schema.Image{ ID: 1, }, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } t.Run("via Get", func(t *testing.T) { image, _, err := env.Client.Image.Get(ctx, "1") if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if image != nil { t.Fatal("expected no image") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my+image" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ImageListResponse{ Images: []schema.Image{ { ID: 1, }, }, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByName(ctx, "my image") if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } t.Run("via Get", func(t *testing.T) { image, _, err := env.Client.Image.Get(ctx, "my image") if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my+image" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ImageListResponse{ Images: []schema.Image{}, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByName(ctx, "my image") if err != nil { t.Fatal(err) } if image != nil { t.Fatal("unexpected image") } }) t.Run("GetByName (empty)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() image, _, err := env.Client.Image.GetByName(ctx, "") if err != nil { t.Fatal(err) } if image != nil { t.Fatal("unexpected image") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ImageListResponse{ Images: []schema.Image{ {ID: 1}, {ID: 2}, }, }) }) opts := ImageListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() images, _, err := env.Client.Image.List(ctx, opts) if err != nil { t.Fatal(err) } if len(images) != 2 { t.Fatal("expected 2 images") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Images []schema.Image `json:"images"` Meta schema.Meta `json:"meta"` }{ Images: []schema.Image{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() images, err := env.Client.Image.All(ctx) if err != nil { t.Fatalf("Image.List failed: %s", err) } if len(images) != 3 { t.Fatalf("expected 3 images; got %d", len(images)) } if images[0].ID != 1 || images[1].ID != 2 || images[2].ID != 3 { t.Errorf("unexpected images") } }) t.Run("AllWithOpts", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } if name := r.URL.Query().Get("name"); name != "my-image" { t.Errorf("unexpected name: %s", name) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Images []schema.Image `json:"images"` Meta schema.Meta `json:"meta"` }{ Images: []schema.Image{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() opts := ImageListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}, Name: "my-image", Type: []ImageType{"backup", "system"}} images, err := env.Client.Image.AllWithOpts(ctx, opts) if err != nil { t.Fatal(err) } if len(images) != 3 { t.Fatalf("expected 3 images; got %d", len(images)) } if images[0].ID != 1 || images[1].ID != 2 || images[2].ID != 3 { t.Errorf("unexpected images") } }) t.Run("Delete", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() image = &Image{ID: 1} ) _, err := env.Client.Image.Delete(ctx, image) if err != nil { t.Fatalf("Image.Delete failed: %s", err) } }) } func TestImageClientUpdate(t *testing.T) { var ( ctx = context.Background() image = &Image{ID: 1} ) t.Run("description and type", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ImageUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description == nil || *reqBody.Description != "test" { t.Errorf("unexpected description: %v", reqBody.Description) } if reqBody.Type == nil || *reqBody.Type != "snapshot" { t.Errorf("unexpected type: %v", reqBody.Type) } json.NewEncoder(w).Encode(schema.ImageUpdateResponse{ Image: schema.Image{ ID: 1, }, }) }) opts := ImageUpdateOpts{ Description: String("test"), Type: ImageTypeSnapshot, } updatedImage, _, err := env.Client.Image.Update(ctx, image, opts) if err != nil { t.Fatal(err) } if updatedImage.ID != 1 { t.Errorf("unexpected image ID: %v", updatedImage.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ImageUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description != nil { t.Errorf("unexpected no description, but got: %v", reqBody.Description) } if reqBody.Type != nil { t.Errorf("unexpected no type, but got: %v", reqBody.Type) } json.NewEncoder(w).Encode(schema.ImageUpdateResponse{ Image: schema.Image{ ID: 1, }, }) }) opts := ImageUpdateOpts{} updatedImage, _, err := env.Client.Image.Update(ctx, image, opts) if err != nil { t.Fatal(err) } if updatedImage.ID != 1 { t.Errorf("unexpected image ID: %v", updatedImage.ID) } }) } func TestImageClientChangeProtection(t *testing.T) { var ( ctx = context.Background() image = &Image{ID: 1} ) t.Run("enable delete protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ImageActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } json.NewEncoder(w).Encode(schema.ImageActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ImageChangeProtectionOpts{ Delete: Bool(true), } action, _, err := env.Client.Image.ChangeProtection(ctx, image, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/internal/000077500000000000000000000000001414442033200240005ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/internal/instrumentation/000077500000000000000000000000001414442033200272435ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/internal/instrumentation/metrics.go000066400000000000000000000062361414442033200312470ustar00rootroot00000000000000package instrumentation import ( "fmt" "net/http" "regexp" "strconv" "strings" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) type Instrumenter struct { subsystemIdentifier string // will be used as part of the metric name (hcloud__requests_total) instrumentationRegistry *prometheus.Registry } // New creates a new Instrumenter. The subsystemIdentifier will be used as part of the metric names (e.g. hcloud__requests_total) func New(subsystemIdentifier string, instrumentationRegistry *prometheus.Registry) *Instrumenter { return &Instrumenter{subsystemIdentifier: subsystemIdentifier, instrumentationRegistry: instrumentationRegistry} } // InstrumentedRoundTripper returns an instrumented round tripper. func (i *Instrumenter) InstrumentedRoundTripper() http.RoundTripper { inFlightRequestsGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: fmt.Sprintf("hcloud_%s_in_flight_requests", i.subsystemIdentifier), Help: fmt.Sprintf("A gauge of in-flight requests to the hcloud %s.", i.subsystemIdentifier), }) requestsPerEndpointCounter := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: fmt.Sprintf("hcloud_%s_requests_total", i.subsystemIdentifier), Help: fmt.Sprintf("A counter for requests to the hcloud %s per endpoint.", i.subsystemIdentifier), }, []string{"code", "method", "api_endpoint"}, ) requestLatencyHistogram := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: fmt.Sprintf("hcloud_%s_request_duration_seconds", i.subsystemIdentifier), Help: fmt.Sprintf("A histogram of request latencies to the hcloud %s .", i.subsystemIdentifier), Buckets: prometheus.DefBuckets, }, []string{"method"}, ) i.instrumentationRegistry.MustRegister(requestsPerEndpointCounter, requestLatencyHistogram, inFlightRequestsGauge) return promhttp.InstrumentRoundTripperInFlight(inFlightRequestsGauge, promhttp.InstrumentRoundTripperDuration(requestLatencyHistogram, i.instrumentRoundTripperEndpoint(requestsPerEndpointCounter, http.DefaultTransport, ), ), ) } // instrumentRoundTripperEndpoint implements a hcloud specific round tripper to count requests per API endpoint // numeric IDs are removed from the URI Path. // Sample: // /volumes/1234/actions/attach --> /volumes/actions/attach func (i *Instrumenter) instrumentRoundTripperEndpoint(counter *prometheus.CounterVec, next http.RoundTripper) promhttp.RoundTripperFunc { return func(r *http.Request) (*http.Response, error) { resp, err := next.RoundTrip(r) if err == nil { statusCode := strconv.Itoa(resp.StatusCode) counter.WithLabelValues(statusCode, strings.ToLower(resp.Request.Method), preparePathForLabel(resp.Request.URL.Path)).Inc() } return resp, err } } func preparePathForLabel(path string) string { path = strings.ToLower(path) // replace all numbers and chars that are not a-z, / or _ reg := regexp.MustCompile("[^a-z/_]+") path = reg.ReplaceAllString(path, "") // replace all artifacts of number replacement (//) path = strings.ReplaceAll(path, "//", "/") // replace the /v/ that indicated the API version return strings.Replace(path, "/v/", "/", 1) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/internal/instrumentation/metrics_test.go000066400000000000000000000010131414442033200322720ustar00rootroot00000000000000package instrumentation import "testing" func Test_preparePath(t *testing.T) { tests := []struct { name string path string want string }{ { "simple test", "/v1/volumes/123456", "/volumes/", }, { "simple test", "/v1/volumes/123456/actions/attach", "/volumes/actions/attach", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := preparePathForLabel(tt.path); got != tt.want { t.Errorf("preparePathForLabel() = %v, want %v", got, tt.want) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/iso.go000066400000000000000000000063121414442033200233070ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/iso_test.go000066400000000000000000000121521414442033200243450ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestISOIsDeprecated(t *testing.T) { t.Run("not deprecated", func(t *testing.T) { iso := &ISO{} if iso.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", iso.IsDeprecated()) } }) t.Run("deprecated", func(t *testing.T) { iso := &ISO{ Deprecated: time.Now(), } if !iso.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", iso.IsDeprecated()) } }) } func TestISOClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ISOGetResponse{ ISO: schema.ISO{ ID: 1, }, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } t.Run("via Get", func(t *testing.T) { iso, _, err := env.Client.ISO.Get(ctx, "1") if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if iso != nil { t.Fatal("expected no iso") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=debian-9" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ISOListResponse{ ISOs: []schema.ISO{ { ID: 1, }, }, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByName(ctx, "debian-9") if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } t.Run("via Get", func(t *testing.T) { iso, _, err := env.Client.ISO.Get(ctx, "debian-9") if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=debian-9" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ISOListResponse{ ISOs: []schema.ISO{}, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByName(ctx, "debian-9") if err != nil { t.Fatal(err) } if iso != nil { t.Fatal("unexpected iso") } }) t.Run("GetByName (empty)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() iso, _, err := env.Client.ISO.GetByName(ctx, "") if err != nil { t.Fatal(err) } if iso != nil { t.Fatal("unexpected iso") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ISOListResponse{ ISOs: []schema.ISO{ {ID: 1}, {ID: 2}, }, }) }) opts := ISOListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() isos, _, err := env.Client.ISO.List(ctx, opts) if err != nil { t.Fatal(err) } if len(isos) != 2 { t.Fatal("expected 2 isos") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { ISOs []schema.ISO `json:"isos"` Meta schema.Meta `json:"meta"` }{ ISOs: []schema.ISO{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() isos, err := env.Client.ISO.All(ctx) if err != nil { t.Fatalf("ISO.List failed: %s", err) } if len(isos) != 3 { t.Fatalf("expected 3 isos; got %d", len(isos)) } if isos[0].ID != 1 || isos[1].ID != 2 || isos[2].ID != 3 { t.Errorf("unexpected isos") } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/load_balancer.go000066400000000000000000001054541414442033200252720ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "fmt" "net" "net/http" "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 DNSPtr string } // LoadBalancerPublicNetIPv6 represents a Load Balancer's public IPv6 address. type LoadBalancerPublicNetIPv6 struct { IP net.IP DNSPtr string } // 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 } // changeDNSPtr changes or resets the reverse DNS pointer for a IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (lb *LoadBalancer) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) { reqBody := schema.LoadBalancerActionChangeDNSPtrRequest{ IP: ip.String(), DNSPtr: ptr, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/load_balancers/%d/actions/change_dns_ptr", lb.ID) req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.LoadBalancerActionChangeDNSPtrResponse{} resp, err := client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // GetDNSPtrForIP searches for the dns assigned to the given IP address. // It returns an error if there is no dns set for the given IP address. func (lb *LoadBalancer) GetDNSPtrForIP(ip net.IP) (string, error) { if net.IP.Equal(lb.PublicNet.IPv4.IP, ip) { return lb.PublicNet.IPv4.DNSPtr, nil } else if net.IP.Equal(lb.PublicNet.IPv6.IP, ip) { return lb.PublicNet.IPv6.DNSPtr, nil } return "", DNSNotFoundError{ip} } // 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 } // LoadBalancerMetricType is the type of available metrics for Load Balancers. type LoadBalancerMetricType string // Available types of Load Balancer metrics. See Hetzner Cloud API // documentation for details. const ( LoadBalancerMetricOpenConnections LoadBalancerMetricType = "open_connections" LoadBalancerMetricConnectionsPerSecond LoadBalancerMetricType = "connections_per_second" LoadBalancerMetricRequestsPerSecond LoadBalancerMetricType = "requests_per_second" LoadBalancerMetricBandwidth LoadBalancerMetricType = "bandwidth" ) // LoadBalancerGetMetricsOpts configures the call to get metrics for a Load // Balancer. type LoadBalancerGetMetricsOpts struct { Types []LoadBalancerMetricType Start time.Time End time.Time Step int } func (o *LoadBalancerGetMetricsOpts) addQueryParams(req *http.Request) error { query := req.URL.Query() if len(o.Types) == 0 { return fmt.Errorf("no metric types specified") } for _, typ := range o.Types { query.Add("type", string(typ)) } if o.Start.IsZero() { return fmt.Errorf("no start time specified") } query.Add("start", o.Start.Format(time.RFC3339)) if o.End.IsZero() { return fmt.Errorf("no end time specified") } query.Add("end", o.End.Format(time.RFC3339)) if o.Step > 0 { query.Add("step", strconv.Itoa(o.Step)) } req.URL.RawQuery = query.Encode() return nil } // LoadBalancerMetrics contains the metrics requested for a Load Balancer. type LoadBalancerMetrics struct { Start time.Time End time.Time Step float64 TimeSeries map[string][]LoadBalancerMetricsValue } // LoadBalancerMetricsValue represents a single value in a time series of metrics. type LoadBalancerMetricsValue struct { Timestamp float64 Value string } // GetMetrics obtains metrics for a Load Balancer. func (c *LoadBalancerClient) GetMetrics( ctx context.Context, lb *LoadBalancer, opts LoadBalancerGetMetricsOpts, ) (*LoadBalancerMetrics, *Response, error) { var respBody schema.LoadBalancerGetMetricsResponse if lb == nil { return nil, nil, fmt.Errorf("illegal argument: load balancer is nil") } path := fmt.Sprintf("/load_balancers/%d/metrics", lb.ID) req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, fmt.Errorf("new request: %v", err) } if err := opts.addQueryParams(req); err != nil { return nil, nil, fmt.Errorf("add query params: %v", err) } resp, err := c.client.Do(req, &respBody) if err != nil { return nil, nil, fmt.Errorf("get metrics: %v", err) } ms, err := loadBalancerMetricsFromSchema(&respBody) if err != nil { return nil, nil, fmt.Errorf("convert response body: %v", err) } return ms, resp, nil } // ChangeDNSPtr changes or resets the reverse DNS pointer for a Load Balancer. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (c *LoadBalancerClient) ChangeDNSPtr(ctx context.Context, lb *LoadBalancer, ip string, ptr *string) (*Action, *Response, error) { netIP := net.ParseIP(ip) if netIP == nil { return nil, nil, InvalidIPError{ip} } return lb.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/load_balancer_test.go000066400000000000000000001025541414442033200263270ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net" "net/http" "net/url" "strconv" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestLoadBalancerClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.LoadBalancerGetResponse{ LoadBalancer: schema.LoadBalancer{ ID: 1, }, }) }) ctx := context.Background() loadBalancer, _, err := env.Client.LoadBalancer.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if loadBalancer == nil { t.Fatal("no load balancer") } if loadBalancer.ID != 1 { t.Errorf("unexpected load balancer ID: %v", loadBalancer.ID) } t.Run("called via Get", func(t *testing.T) { loadBalancer, _, err := env.Client.LoadBalancer.Get(ctx, "1") if err != nil { t.Fatal(err) } if loadBalancer == nil { t.Fatal("no load balancer") } if loadBalancer.ID != 1 { t.Errorf("unexpected load balancer ID: %v", loadBalancer.ID) } }) } func TestLoadBalancerClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() loadBalancer, _, err := env.Client.LoadBalancer.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if loadBalancer != nil { t.Fatal("expected no load balancer") } } func TestLoadBalancerClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mylb" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LoadBalancerListResponse{ LoadBalancers: []schema.LoadBalancer{ { ID: 1, Name: "mylb", }, }, }) }) ctx := context.Background() loadBalancer, _, err := env.Client.LoadBalancer.GetByName(ctx, "mylb") if err != nil { t.Fatal(err) } if loadBalancer == nil { t.Fatal("no load balancer") } if loadBalancer.ID != 1 { t.Errorf("unexpected load balancer ID: %v", loadBalancer.ID) } t.Run("via Get", func(t *testing.T) { loadBalancer, _, err := env.Client.LoadBalancer.Get(ctx, "mylb") if err != nil { t.Fatal(err) } if loadBalancer == nil { t.Fatal("no load balancer") } if loadBalancer.ID != 1 { t.Errorf("unexpected load balancer ID: %v", loadBalancer.ID) } }) } func TestLoadBalancerClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mylb" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LoadBalancerListResponse{ LoadBalancers: []schema.LoadBalancer{}, }) }) ctx := context.Background() loadBalancer, _, err := env.Client.LoadBalancer.GetByName(ctx, "mylb") if err != nil { t.Fatal(err) } if loadBalancer != nil { t.Fatal("unexpected load balancer") } } func TestLoadBalancerClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() loadBalancer, _, err := env.Client.LoadBalancer.GetByName(ctx, "") if err != nil { t.Fatal(err) } if loadBalancer != nil { t.Fatal("unexpected load balancer") } } func TestLoadBalancerCreate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.LoadBalancerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerCreateRequest{ Name: "load-balancer", LoadBalancerType: "lb1", Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{ Type: "round_robin", }, Location: String("fsn1"), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerCreateResponse{ LoadBalancer: schema.LoadBalancer{ID: 2}, Action: schema.Action{ID: 1}, }) }) var ( ctx = context.Background() lbType = &LoadBalancerType{Name: "lb1"} algorithm = &LoadBalancerAlgorithm{Type: LoadBalancerAlgorithmTypeRoundRobin} location = &Location{Name: "fsn1"} opts = LoadBalancerCreateOpts{ Name: "load-balancer", LoadBalancerType: lbType, Algorithm: algorithm, Location: location, } ) result, _, err := env.Client.LoadBalancer.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.LoadBalancer.ID != 2 { t.Errorf("unexpected load balancer ID: %d", result.LoadBalancer.ID) } } func TestLoadBalancerDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) _, err := env.Client.LoadBalancer.Delete(ctx, loadBalancer) if err != nil { t.Fatal(err) } } func TestLoadBalancerClientUpdate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.LoadBalancerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerUpdateRequest{ Name: String("test"), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerUpdateResponse{ LoadBalancer: schema.LoadBalancer{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) opts := LoadBalancerUpdateOpts{ Name: "test", } updatedLoadBalancer, _, err := env.Client.LoadBalancer.Update(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if updatedLoadBalancer.ID != 1 { t.Errorf("unexpected load balancer ID: %v", updatedLoadBalancer.ID) } } func TestLoadBalancerClientChangeProtection(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionChangeProtectionRequest{ Delete: Bool(true), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) opts := LoadBalancerChangeProtectionOpts{ Delete: Bool(true), } action, _, err := env.Client.LoadBalancer.ChangeProtection(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestLoadBalancerClientAddServerTarget(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/add_target", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionAddTargetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionAddTargetRequest{ Type: string(LoadBalancerTargetTypeServer), Server: &schema.LoadBalancerActionAddTargetRequestServer{ ID: 1, }, UsePrivateIP: Bool(true), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionAddTargetResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} server = &Server{ID: 1} ) opts := LoadBalancerAddServerTargetOpts{ Server: server, UsePrivateIP: Bool(true), } action, _, err := env.Client.LoadBalancer.AddServerTarget(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerClientRemoveServerTarget(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/remove_target", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionRemoveTargetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionRemoveTargetRequest{ Type: string(LoadBalancerTargetTypeServer), Server: &schema.LoadBalancerActionRemoveTargetRequestServer{ ID: 1, }, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionRemoveTargetResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} server = &Server{ID: 1} ) action, _, err := env.Client.LoadBalancer.RemoveServerTarget(ctx, loadBalancer, server) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerAddService(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/add_service", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionAddServiceRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionAddServiceRequest{ Protocol: string(LoadBalancerServiceProtocolHTTP), ListenPort: Int(4711), DestinationPort: Int(80), HTTP: &schema.LoadBalancerActionAddServiceRequestHTTP{ CookieName: String("HCLBSTICKY"), CookieLifetime: Int(5 * 60), RedirectHTTP: Bool(false), StickySessions: Bool(true), }, HealthCheck: &schema.LoadBalancerActionAddServiceRequestHealthCheck{ Protocol: "http", Port: Int(4711), Interval: Int(15), Timeout: Int(10), Retries: Int(3), HTTP: &schema.LoadBalancerActionAddServiceRequestHealthCheckHTTP{ Domain: String("example.com"), Path: String("/"), }, }, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionAddServiceResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) opts := LoadBalancerAddServiceOpts{ Protocol: LoadBalancerServiceProtocolHTTP, ListenPort: Int(4711), DestinationPort: Int(80), HTTP: &LoadBalancerAddServiceOptsHTTP{ CookieName: String("HCLBSTICKY"), CookieLifetime: Duration(5 * time.Minute), RedirectHTTP: Bool(false), StickySessions: Bool(true), }, HealthCheck: &LoadBalancerAddServiceOptsHealthCheck{ Protocol: "http", Port: Int(4711), Interval: Duration(15 * time.Second), Timeout: Duration(10 * time.Second), Retries: Int(3), HTTP: &LoadBalancerAddServiceOptsHealthCheckHTTP{ Domain: String("example.com"), Path: String("/"), }, }, } action, _, err := env.Client.LoadBalancer.AddService(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestLoadBalancerUpdateService(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/update_service", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionUpdateServiceRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionUpdateServiceRequest{ Protocol: String(string(LoadBalancerServiceProtocolHTTP)), ListenPort: 4711, DestinationPort: Int(80), HTTP: &schema.LoadBalancerActionUpdateServiceRequestHTTP{ CookieName: String("HCLBSTICKY"), CookieLifetime: Int(5 * 60), RedirectHTTP: Bool(false), StickySessions: Bool(true), }, HealthCheck: &schema.LoadBalancerActionUpdateServiceRequestHealthCheck{ Protocol: String(string(LoadBalancerServiceProtocolHTTP)), Port: Int(4711), Interval: Int(15), Timeout: Int(10), Retries: Int(3), HTTP: &schema.LoadBalancerActionUpdateServiceRequestHealthCheckHTTP{ Domain: String("example.com"), Path: String("/"), }, }, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionUpdateServiceResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) opts := LoadBalancerUpdateServiceOpts{ Protocol: LoadBalancerServiceProtocolHTTP, DestinationPort: Int(80), HTTP: &LoadBalancerUpdateServiceOptsHTTP{ CookieName: String("HCLBSTICKY"), CookieLifetime: Duration(5 * time.Minute), RedirectHTTP: Bool(false), StickySessions: Bool(true), }, HealthCheck: &LoadBalancerUpdateServiceOptsHealthCheck{ Protocol: LoadBalancerServiceProtocolHTTP, Port: Int(4711), Interval: Duration(15 * time.Second), Timeout: Duration(10 * time.Second), Retries: Int(3), HTTP: &LoadBalancerUpdateServiceOptsHealthCheckHTTP{ Domain: String("example.com"), Path: String("/"), }, }, } action, _, err := env.Client.LoadBalancer.UpdateService(ctx, loadBalancer, 4711, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestLoadBalancerDeleteService(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/delete_service", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerDeleteServiceRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerDeleteServiceRequest{ ListenPort: 4711, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerDeleteServiceResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) action, _, err := env.Client.LoadBalancer.DeleteService(ctx, loadBalancer, 4711) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerClientChangeAlgorithm(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/change_algorithm", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionChangeAlgorithmRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionChangeAlgorithmRequest{ Type: string(LoadBalancerAlgorithmTypeRoundRobin), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeAlgorithmResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) opts := LoadBalancerChangeAlgorithmOpts{ Type: LoadBalancerAlgorithmTypeRoundRobin, } action, _, err := env.Client.LoadBalancer.ChangeAlgorithm(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestLoadBalancerClientAttachToNetwork(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/attach_to_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionAttachToNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionAttachToNetworkRequest{ Network: 1, IP: String("10.0.1.1"), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} network = &Network{ID: 1} ) opts := LoadBalancerAttachToNetworkOpts{ Network: network, IP: net.ParseIP("10.0.1.1"), } action, _, err := env.Client.LoadBalancer.AttachToNetwork(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestLoadBalancerClientDetachFromNetwork(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/detach_from_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionDetachFromNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.LoadBalancerActionDetachFromNetworkRequest{ Network: 1, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.LoadBalancerActionDetachFromNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} network = &Network{ID: 1} ) opts := LoadBalancerDetachFromNetworkOpts{ Network: network, } action, _, err := env.Client.LoadBalancer.DetachFromNetwork(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestLoadBalancerClientEnablePublicInterface(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/enable_public_interface", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.LoadBalancerActionEnablePublicInterfaceResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) action, _, err := env.Client.LoadBalancer.EnablePublicInterface(ctx, loadBalancer) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerClientDisablePublicInterface(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/disable_public_interface", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.LoadBalancerActionDisablePublicInterfaceResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) action, _, err := env.Client.LoadBalancer.DisablePublicInterface(ctx, loadBalancer) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerClientChangeType(t *testing.T) { var ( ctx = context.Background() loadBalancer = &LoadBalancer{ID: 1} ) t.Run("with Load Balancer type ID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/change_type", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.LoadBalancerActionChangeTypeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if id, ok := reqBody.LoadBalancerType.(float64); !ok || id != 1 { t.Errorf("unexpected Load Balancer type ID: %v", reqBody.LoadBalancerType) } json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeTypeResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := LoadBalancerChangeTypeOpts{ LoadBalancerType: &LoadBalancerType{ID: 1}, } action, _, err := env.Client.LoadBalancer.ChangeType(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("with Load Balancer type name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/change_type", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.LoadBalancerActionChangeTypeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if name, ok := reqBody.LoadBalancerType.(string); !ok || name != "type" { t.Errorf("unexpected Load Balancer type name: %v", reqBody.LoadBalancerType) } json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeTypeResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := LoadBalancerChangeTypeOpts{ LoadBalancerType: &LoadBalancerType{Name: "type"}, } action, _, err := env.Client.LoadBalancer.ChangeType(ctx, loadBalancer, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestLoadBalancerClientAddLabelSelectorTarget(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/add_target", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionAddTargetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != string(LoadBalancerTargetTypeLabelSelector) { t.Errorf("unexpected type %v", reqBody.Type) } if reqBody.LabelSelector.Selector != "key=value" { t.Errorf("unexpected LabelSelector %v", reqBody.LabelSelector) } if *reqBody.UsePrivateIP != false { t.Errorf("unexpected UsePrivateIP %v", reqBody.UsePrivateIP) } json.NewEncoder(w).Encode(schema.LoadBalancerActionAddTargetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.LoadBalancer.AddLabelSelectorTarget(ctx, &LoadBalancer{ID: 1}, LoadBalancerAddLabelSelectorTargetOpts{ Selector: "key=value", UsePrivateIP: Bool(false), }) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerClientRemoveLabelSelectorTarget(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/remove_target", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionRemoveTargetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != string(LoadBalancerTargetTypeLabelSelector) { t.Errorf("unexpected type %v", reqBody.Type) } if reqBody.LabelSelector.Selector != "key=value" { t.Errorf("unexpected LabelSelector %v", reqBody.LabelSelector) } json.NewEncoder(w).Encode(schema.LoadBalancerActionRemoveTargetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.LoadBalancer.RemoveLabelSelectorTarget(ctx, &LoadBalancer{ID: 1}, "key=value") if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerClientAddIPTarget(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/add_target", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.LoadBalancerActionAddTargetRequest if r.Method != "POST" { t.Error("expected POST") } if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != string(LoadBalancerTargetTypeIP) { t.Errorf("unexpected type %v", reqBody.Type) } if reqBody.IP.IP != "1.2.3.4" { t.Errorf("unexpected IP target %v", reqBody.IP) } json.NewEncoder(w).Encode(schema.LoadBalancerActionAddTargetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.LoadBalancer.AddIPTarget(ctx, &LoadBalancer{ID: 1}, LoadBalancerAddIPTargetOpts{ IP: net.ParseIP("1.2.3.4"), }) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerClientRemoveIPTarget(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancers/1/actions/remove_target", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.LoadBalancerActionRemoveTargetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != string(LoadBalancerTargetTypeIP) { t.Errorf("unexpected type %v", reqBody.Type) } if reqBody.IP.IP != "1.2.3.4" { t.Errorf("unexpected IP %v", reqBody.IP) } json.NewEncoder(w).Encode(schema.LoadBalancerActionRemoveTargetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.LoadBalancer.RemoveIPTarget(ctx, &LoadBalancer{ID: 1}, net.ParseIP("1.2.3.4")) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestLoadBalancerGetMetrics(t *testing.T) { tests := []struct { name string lb *LoadBalancer opts LoadBalancerGetMetricsOpts respStatus int respFn func() schema.LoadBalancerGetMetricsResponse expected LoadBalancerMetrics expectedErr string }{ { name: "all metrics", lb: &LoadBalancer{ID: 2}, opts: LoadBalancerGetMetricsOpts{ Types: []LoadBalancerMetricType{ LoadBalancerMetricOpenConnections, LoadBalancerMetricConnectionsPerSecond, LoadBalancerMetricRequestsPerSecond, LoadBalancerMetricBandwidth, }, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, respFn: func() schema.LoadBalancerGetMetricsResponse { var resp schema.LoadBalancerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{ "open_connections": { Values: []interface{}{ []interface{}{1435781470.622, "42"}, []interface{}{1435781471.622, "43"}, }, }, "connections_per_second": { Values: []interface{}{ []interface{}{1435781480.622, "100"}, []interface{}{1435781481.622, "150"}, }, }, "requests_per_second": { Values: []interface{}{ []interface{}{1435781480.622, "50"}, []interface{}{1435781481.622, "55"}, }, }, "bandwidth.in": { Values: []interface{}{ []interface{}{1435781490.622, "70"}, []interface{}{1435781491.622, "75"}, }, }, "bandwidth.out": { Values: []interface{}{ []interface{}{1435781590.622, "60"}, []interface{}{1435781591.622, "65"}, }, }, } return resp }, expected: LoadBalancerMetrics{ Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), TimeSeries: map[string][]LoadBalancerMetricsValue{ "open_connections": { {Timestamp: 1435781470.622, Value: "42"}, {Timestamp: 1435781471.622, Value: "43"}, }, "connections_per_second": { {Timestamp: 1435781480.622, Value: "100"}, {Timestamp: 1435781481.622, Value: "150"}, }, "requests_per_second": { {Timestamp: 1435781480.622, Value: "50"}, {Timestamp: 1435781481.622, Value: "55"}, }, "bandwidth.in": { {Timestamp: 1435781490.622, Value: "70"}, {Timestamp: 1435781491.622, Value: "75"}, }, "bandwidth.out": { {Timestamp: 1435781590.622, Value: "60"}, {Timestamp: 1435781591.622, Value: "65"}, }, }, }, }, { name: "missing metrics types", lb: &LoadBalancer{ID: 3}, opts: LoadBalancerGetMetricsOpts{ Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, expectedErr: "add query params: no metric types specified", }, { name: "no start time", lb: &LoadBalancer{ID: 4}, opts: LoadBalancerGetMetricsOpts{ Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth}, End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, expectedErr: "add query params: no start time specified", }, { name: "no end time", lb: &LoadBalancer{ID: 5}, opts: LoadBalancerGetMetricsOpts{ Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth}, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), }, expectedErr: "add query params: no end time specified", }, { name: "call to backend API fails", lb: &LoadBalancer{ID: 6}, opts: LoadBalancerGetMetricsOpts{ Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth}, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, respStatus: http.StatusInternalServerError, expectedErr: "get metrics: hcloud: server responded with status code 500", }, { name: "no load balancer passed", opts: LoadBalancerGetMetricsOpts{ Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth}, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, expectedErr: "illegal argument: load balancer is nil", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { env := newTestEnv() defer env.Teardown() if tt.lb != nil { path := fmt.Sprintf("/load_balancers/%d/metrics", tt.lb.ID) env.Mux.HandleFunc(path, func(rw http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Errorf("expected GET; got %s", r.Method) } opts := loadBalancerMetricsOptsFromURL(t, r.URL) if !cmp.Equal(tt.opts, opts) { t.Errorf("unexpected opts: url: %s\n%v", r.URL.String(), cmp.Diff(tt.opts, opts)) } status := tt.respStatus if status == 0 { status = http.StatusOK } rw.WriteHeader(status) if tt.respFn != nil { resp := tt.respFn() if err := json.NewEncoder(rw).Encode(resp); err != nil { t.Errorf("failed to encode response: %v", err) } } }) } ctx := context.Background() actual, _, err := env.Client.LoadBalancer.GetMetrics(ctx, tt.lb, tt.opts) if tt.expectedErr != "" { if tt.expectedErr != err.Error() { t.Errorf("expected err: %v; got: %v", tt.expectedErr, err) } return } if err != nil { t.Fatalf("failed to get load balancer metrics: %v", err) } if !cmp.Equal(&tt.expected, actual) { t.Errorf("Actual metrics did not equal expected: %s", cmp.Diff(&tt.expected, actual)) } }) } } func loadBalancerMetricsOptsFromURL(t *testing.T, u *url.URL) LoadBalancerGetMetricsOpts { var opts LoadBalancerGetMetricsOpts for k, vs := range u.Query() { switch k { case "type": for _, v := range vs { opts.Types = append(opts.Types, LoadBalancerMetricType(v)) } case "start": if len(vs) != 1 { t.Errorf("expected one value for start; got %d: %v", len(vs), vs) continue } v, err := time.Parse(time.RFC3339, vs[0]) if err != nil { t.Errorf("parse start as RFC3339: %v", err) } opts.Start = v case "end": if len(vs) != 1 { t.Errorf("expected one value for end; got %d: %v", len(vs), vs) continue } v, err := time.Parse(time.RFC3339, vs[0]) if err != nil { t.Errorf("parse end as RFC3339: %v", err) } opts.End = v case "step": if len(vs) != 1 { t.Errorf("expected one value for step; got %d: %v", len(vs), vs) continue } v, err := strconv.Atoi(vs[0]) if err != nil { t.Errorf("invalid step: %v", err) } opts.Step = v } } return opts } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/load_balancer_type.go000066400000000000000000000075261414442033200263340ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/load_balancer_type_test.go000066400000000000000000000133651414442033200273710ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestLoadBalancerTypeClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancer_types/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.LoadBalancerTypeGetResponse{ LoadBalancerType: schema.LoadBalancerType{ ID: 1, }, }) }) ctx := context.Background() loadBalancerType, _, err := env.Client.LoadBalancerType.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if loadBalancerType == nil { t.Fatal("no load balancer type") } if loadBalancerType.ID != 1 { t.Errorf("unexpected load balancer type ID: %v", loadBalancerType.ID) } t.Run("via Get", func(t *testing.T) { loadBalancerType, _, err := env.Client.LoadBalancerType.Get(ctx, "1") if err != nil { t.Fatal(err) } if loadBalancerType == nil { t.Fatal("no load balancer type") } if loadBalancerType.ID != 1 { t.Errorf("unexpected load balancer type ID: %v", loadBalancerType.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancer_types/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() loadBalancerType, _, err := env.Client.LoadBalancerType.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if loadBalancerType != nil { t.Fatal("expected no load balancer type") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancer_types", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=lb1" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LoadBalancerTypeListResponse{ LoadBalancerTypes: []schema.LoadBalancerType{ { ID: 1, }, }, }) }) ctx := context.Background() loadBalancerType, _, err := env.Client.LoadBalancerType.GetByName(ctx, "lb1") if err != nil { t.Fatal(err) } if loadBalancerType == nil { t.Fatal("no load balancer type") } if loadBalancerType.ID != 1 { t.Errorf("unexpected load balancer type ID: %v", loadBalancerType.ID) } t.Run("via Get", func(t *testing.T) { loadBalancerType, _, err := env.Client.LoadBalancerType.Get(ctx, "lb1") if err != nil { t.Fatal(err) } if loadBalancerType == nil { t.Fatal("no load balancer type") } if loadBalancerType.ID != 1 { t.Errorf("unexpected load balancer type ID: %v", loadBalancerType.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancer_types", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=lb1" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LoadBalancerTypeListResponse{ LoadBalancerTypes: []schema.LoadBalancerType{}, }) }) ctx := context.Background() loadBalancerType, _, err := env.Client.LoadBalancerType.GetByName(ctx, "lb1") if err != nil { t.Fatal(err) } if loadBalancerType != nil { t.Fatal("unexpected load balancer type") } }) t.Run("GetByName (empty)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() loadBalancerType, _, err := env.Client.LoadBalancerType.GetByName(ctx, "") if err != nil { t.Fatal(err) } if loadBalancerType != nil { t.Fatal("unexpected load balancer type") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancer_types", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.LoadBalancerTypeListResponse{ LoadBalancerTypes: []schema.LoadBalancerType{ {ID: 1}, {ID: 2}, }, }) }) opts := LoadBalancerTypeListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() loadBalancerTypes, _, err := env.Client.LoadBalancerType.List(ctx, opts) if err != nil { t.Fatal(err) } if len(loadBalancerTypes) != 2 { t.Fatal("expected 2 load balancer type") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/load_balancer_types", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { LoadBalancerTypes []schema.LoadBalancerType `json:"load_balancer_types"` Meta schema.Meta `json:"meta"` }{ LoadBalancerTypes: []schema.LoadBalancerType{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() loadBalancerTypes, err := env.Client.LoadBalancerType.All(ctx) if err != nil { t.Fatalf("LoadBalancerType.List failed: %s", err) } if len(loadBalancerTypes) != 3 { t.Fatalf("expected 3 load balancer types; got %d", len(loadBalancerTypes)) } if loadBalancerTypes[0].ID != 1 || loadBalancerTypes[1].ID != 2 || loadBalancerTypes[2].ID != 3 { t.Errorf("unexpected load balancer types") } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/location.go000066400000000000000000000064231414442033200243300ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/location_test.go000066400000000000000000000123421414442033200253640ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestLocationClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.LocationGetResponse{ Location: schema.Location{ ID: 1, }, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } t.Run("via Get", func(t *testing.T) { location, _, err := env.Client.Location.Get(ctx, "1") if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if location != nil { t.Fatal("expected no location") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LocationListResponse{ Locations: []schema.Location{ { ID: 1, }, }, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } t.Run("via Get", func(t *testing.T) { location, _, err := env.Client.Location.Get(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LocationListResponse{ Locations: []schema.Location{}, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if location != nil { t.Fatal("unexpected location") } }) t.Run("GetByName (empty)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() location, _, err := env.Client.Location.GetByName(ctx, "") if err != nil { t.Fatal(err) } if location != nil { t.Fatal("unexpected location") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } if name := r.URL.Query().Get("name"); name != "fsn1" { t.Errorf("expected name fsn1; got %q", name) } json.NewEncoder(w).Encode(schema.LocationListResponse{ Locations: []schema.Location{ {ID: 1}, {ID: 2}, }, }) }) opts := LocationListOpts{} opts.Page = 2 opts.PerPage = 50 opts.Name = "fsn1" ctx := context.Background() locations, _, err := env.Client.Location.List(ctx, opts) if err != nil { t.Fatal(err) } if len(locations) != 2 { t.Fatal("expected 2 locations") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Locations []schema.Location `json:"locations"` Meta schema.Meta `json:"meta"` }{ Locations: []schema.Location{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() locations, err := env.Client.Location.All(ctx) if err != nil { t.Fatalf("Location.List failed: %s", err) } if len(locations) != 3 { t.Fatalf("expected 3 locations; got %d", len(locations)) } if locations[0].ID != 1 || locations[1].ID != 2 || locations[2].ID != 3 { t.Errorf("unexpected locations") } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/metadata/000077500000000000000000000000001414442033200237445ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/metadata/client.go000066400000000000000000000071101414442033200255500ustar00rootroot00000000000000package metadata import ( "io/ioutil" "net" "net/http" "strconv" "strings" "github.com/hetznercloud/hcloud-go/hcloud/internal/instrumentation" "github.com/prometheus/client_golang/prometheus" ) const Endpoint = "http://169.254.169.254/hetzner/v1/metadata" // Client is a client for the Hetzner Cloud Server Metadata Endpoints. type Client struct { endpoint string httpClient *http.Client instrumentationRegistry *prometheus.Registry } // A ClientOption is used to configure a Client. type ClientOption func(*Client) // WithEndpoint configures a Client to use the specified Metadata API endpoint. func WithEndpoint(endpoint string) ClientOption { return func(client *Client) { client.endpoint = strings.TrimRight(endpoint, "/") } } // WithHTTPClient configures a Client to perform HTTP requests with httpClient. func WithHTTPClient(httpClient *http.Client) ClientOption { return func(client *Client) { client.httpClient = httpClient } } // WithInstrumentation configures a Client to collect metrics about the performed HTTP requests. func WithInstrumentation(registry *prometheus.Registry) ClientOption { return func(client *Client) { client.instrumentationRegistry = registry } } // NewClient creates a new client. func NewClient(options ...ClientOption) *Client { client := &Client{ endpoint: Endpoint, httpClient: &http.Client{}, } for _, option := range options { option(client) } if client.instrumentationRegistry != nil { i := instrumentation.New("metadata", client.instrumentationRegistry) client.httpClient.Transport = i.InstrumentedRoundTripper() } 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) get(path string) (string, error) { url := c.endpoint + path resp, err := c.httpClient.Get(url) if err != nil { return "", err } body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } resp.Body.Close() return string(body), nil } // IsHcloudServer checks if the currently called server is a hcloud server by calling a metadata endpoint // if the endpoint answers with a non-empty value this method returns true, otherwise false func (c *Client) IsHcloudServer() bool { hostname, err := c.Hostname() if err != nil { return false } if len(hostname) > 0 { return true } return false } // Hostname returns the hostname of the server that did the request to the Metadata server func (c *Client) Hostname() (string, error) { return c.get("/hostname") } // InstanceID returns the ID of the server that did the request to the Metadata server func (c *Client) InstanceID() (int, error) { resp, err := c.get("/instance-id") if err != nil { return 0, err } return strconv.Atoi(resp) } // PublicIPv4 returns the Public IPv4 of the server that did the request to the Metadata server func (c *Client) PublicIPv4() (net.IP, error) { resp, err := c.get("/public-ipv4") if err != nil { return nil, err } return net.ParseIP(resp), nil } // Region returns the Network Zone of the server that did the request to the Metadata server func (c *Client) Region() (string, error) { return c.get("/region") } // AvailabilityZone returns the datacenter of the server that did the request to the Metadata server func (c *Client) AvailabilityZone() (string, error) { return c.get("/availability-zone") } // PrivateNetworks returns details about the private networks the server is attached to // Returns YAML (unparsed) func (c *Client) PrivateNetworks() (string, error) { return c.get("/private-networks") } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/metadata/client_test.go000066400000000000000000000102561414442033200266140ustar00rootroot00000000000000package metadata import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) type testEnv struct { Server *httptest.Server Mux *http.ServeMux Client *Client } func (env *testEnv) Teardown() { env.Server.Close() env.Server = nil env.Mux = nil env.Client = nil } func newTestEnv() testEnv { mux := http.NewServeMux() server := httptest.NewServer(mux) client := NewClient( WithEndpoint(server.URL), ) return testEnv{ Server: server, Mux: mux, Client: client, } } func TestClient_IsHcloudServer(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("my-server")) }) isHcloudServer := env.Client.IsHcloudServer() assert.True(t, isHcloudServer) } func TestClient_IsHcloudServer_EmptyReturn(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("")) }) isHcloudServer := env.Client.IsHcloudServer() assert.False(t, isHcloudServer) } func TestClient_IsHcloudServer_HttpError(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }) isHcloudServer := env.Client.IsHcloudServer() assert.False(t, isHcloudServer) } func TestClient_Hostname(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("my-server")) }) hostname, err := env.Client.Hostname() if err != nil { t.Fatal(err) } if hostname != "my-server" { t.Fatalf("Unexpected hostname %s", hostname) } } func TestClient_InstanceID(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/instance-id", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("123456")) }) instanceID, err := env.Client.InstanceID() if err != nil { t.Fatal(err) } if instanceID != 123456 { t.Fatalf("Unexpected instanceID %d", instanceID) } } func TestClient_PublicIPv4(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/public-ipv4", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("127.0.0.1")) }) publicIPv4, err := env.Client.PublicIPv4() if err != nil { t.Fatal(err) } if publicIPv4.String() != "127.0.0.1" { t.Fatalf("Unexpected PublicIPv4 %s", publicIPv4.String()) } } func TestClient_Region(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/region", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("eu-central")) }) region, err := env.Client.Region() if err != nil { t.Fatal(err) } if region != "eu-central" { t.Fatalf("Unexpected region %s", region) } } func TestClient_AvailabilityZone(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/availability-zone", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("fsn1-dc14")) }) availabilityZone, err := env.Client.AvailabilityZone() if err != nil { t.Fatal(err) } if availabilityZone != "fsn1-dc14" { t.Fatalf("Unexpected availabilityZone %s", availabilityZone) } } func TestClient_PrivateNetworks(t *testing.T) { env := newTestEnv() env.Mux.HandleFunc("/private-networks", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`- ip: 10.0.0.2 alias_ips: [10.0.0.3, 10.0.0.4] interface_num: 1 mac_address: 86:00:00:2a:7d:e0 network_id: 1234 network_name: nw-test1 network: 10.0.0.0/8 subnet: 10.0.0.0/24 gateway: 10.0.0.1 - ip: 192.168.0.2 alias_ips: [] interface_num: 2 mac_address: 86:00:00:2a:7d:e1 network_id: 4321 network_name: nw-test2 network: 192.168.0.0/16 subnet: 192.168.0.0/24 gateway: 192.168.0.1`)) }) privateNetworks, err := env.Client.PrivateNetworks() if err != nil { t.Fatal(err) } expectedNetworks := `- ip: 10.0.0.2 alias_ips: [10.0.0.3, 10.0.0.4] interface_num: 1 mac_address: 86:00:00:2a:7d:e0 network_id: 1234 network_name: nw-test1 network: 10.0.0.0/8 subnet: 10.0.0.0/24 gateway: 10.0.0.1 - ip: 192.168.0.2 alias_ips: [] interface_num: 2 mac_address: 86:00:00:2a:7d:e1 network_id: 4321 network_name: nw-test2 network: 192.168.0.0/16 subnet: 192.168.0.0/24 gateway: 192.168.0.1` assert.Equal(t, privateNetworks, expectedNetworks) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/network.go000066400000000000000000000315771414442033200242210ustar00rootroot00000000000000package 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" NetworkZoneUSEast NetworkZone = "us-east" ) // NetworkSubnetType specifies a type of a subnet. type NetworkSubnetType string // List of available network subnet types. const ( NetworkSubnetTypeCloud NetworkSubnetType = "cloud" NetworkSubnetTypeServer NetworkSubnetType = "server" NetworkSubnetTypeVSwitch NetworkSubnetType = "vswitch" ) // 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 VSwitchID int } // 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 { s := schema.NetworkSubnet{ Type: string(subnet.Type), IPRange: subnet.IPRange.String(), NetworkZone: string(subnet.NetworkZone), } if subnet.VSwitchID != 0 { s.VSwitchID = subnet.VSwitchID } reqBody.Subnets = append(reqBody.Subnets, s) } 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() } if opts.Subnet.VSwitchID != 0 { reqBody.VSwitchID = opts.Subnet.VSwitchID } 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/network_test.go000066400000000000000000000365651414442033200252620ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestNetworkClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkGetResponse{ Network: schema.Network{ ID: 1, }, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } t.Run("called via Get", func(t *testing.T) { network, _, err := env.Client.Network.Get(ctx, "1") if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } }) } func TestNetworkClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if network != nil { t.Fatal("expected no network") } } func TestNetworkClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mynet" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.NetworkListResponse{ Networks: []schema.Network{ { ID: 1, Name: "mynet", }, }, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByName(ctx, "mynet") if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } t.Run("via Get", func(t *testing.T) { network, _, err := env.Client.Network.Get(ctx, "mynet") if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } }) } func TestNetworkClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mynet" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.NetworkListResponse{ Networks: []schema.Network{}, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByName(ctx, "mynet") if err != nil { t.Fatal(err) } if network != nil { t.Fatal("unexpected network") } } func TestNetworkClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() network, _, err := env.Client.Network.GetByName(ctx, "") if err != nil { t.Fatal(err) } if network != nil { t.Fatal("unexpected network") } } func TestNetworkCreate(t *testing.T) { var ( ctx = context.Background() _, ipRange, _ = net.ParseCIDR("10.0.1.0/24") ) t.Run("missing required field name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() opts := NetworkCreateOpts{} _, _, err := env.Client.Network.Create(ctx, opts) if err == nil || err.Error() != "missing name" { t.Fatalf("Network.Create should fail with \"missing name\" but failed with %s", err) } }) t.Run("missing required field ip range", func(t *testing.T) { env := newTestEnv() defer env.Teardown() opts := NetworkCreateOpts{ Name: "my-network", } _, _, err := env.Client.Network.Create(ctx, opts) if err == nil || err.Error() != "missing IP range" { t.Fatalf("Network.Create should fail with \"missing IP range\" but failed with %s", err) } }) t.Run("required fields", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "my-network" { t.Errorf("unexpected Name: %v", reqBody.Name) } if reqBody.IPRange != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.NetworkCreateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkCreateOpts{ Name: "my-network", IPRange: ipRange, } _, _, err := env.Client.Network.Create(ctx, opts) if err != nil { t.Fatal(err) } }) } func TestNetworkDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() network = &Network{ID: 1} ) _, err := env.Client.Network.Delete(ctx, network) if err != nil { t.Fatal(err) } } func TestNetworkClientUpdate(t *testing.T) { var ( ctx = context.Background() network = &Network{ID: 1} ) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.NetworkUpdateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkUpdateOpts{ Name: "test", } updatedNetwork, _, err := env.Client.Network.Update(ctx, network, opts) if err != nil { t.Fatal(err) } if updatedNetwork.ID != 1 { t.Errorf("unexpected network ID: %v", updatedNetwork.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.NetworkUpdateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkUpdateOpts{ Labels: map[string]string{"key": "value"}, } updatedNetwork, _, err := env.Client.Network.Update(ctx, network, opts) if err != nil { t.Fatal(err) } if updatedNetwork.ID != 1 { t.Errorf("unexpected network ID: %v", updatedNetwork.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.NetworkUpdateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkUpdateOpts{} updatedNetwork, _, err := env.Client.Network.Update(ctx, network, opts) if err != nil { t.Fatal(err) } if updatedNetwork.ID != 1 { t.Errorf("unexpected network ID: %v", updatedNetwork.ID) } }) } func TestNetworkClientChangeIPRange(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/change_ip_range", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkActionChangeIPRangeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.IPRange != "10.0.1.0/24" { t.Errorf("unexpected type: %v", reqBody.IPRange) } json.NewEncoder(w).Encode(schema.NetworkActionChangeIPRangeResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, newIPRange, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkChangeIPRangeOpts{ IPRange: newIPRange, } action, _, err := env.Client.Network.ChangeIPRange(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientAddSubnet(t *testing.T) { t.Run("type server with ip range", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/add_subnet", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkActionAddSubnetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != "cloud" { t.Errorf("unexpected Type: %v", reqBody.Type) } if reqBody.IPRange != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", reqBody.IPRange) } if reqBody.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", reqBody.NetworkZone) } json.NewEncoder(w).Encode(schema.NetworkActionAddSubnetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, ipRange, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkAddSubnetOpts{ Subnet: NetworkSubnet{ Type: NetworkSubnetTypeCloud, IPRange: ipRange, NetworkZone: NetworkZoneEUCentral, }, } action, _, err := env.Client.Network.AddSubnet(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("type server without ip range", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/add_subnet", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkActionAddSubnetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != "cloud" { t.Errorf("unexpected Type: %v", reqBody.Type) } if reqBody.IPRange != "" { t.Errorf("unexpected IPRange: %v", reqBody.IPRange) } if reqBody.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", reqBody.NetworkZone) } json.NewEncoder(w).Encode(schema.NetworkActionAddSubnetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() opts := NetworkAddSubnetOpts{ Subnet: NetworkSubnet{ Type: NetworkSubnetTypeCloud, NetworkZone: NetworkZoneEUCentral, }, } action, _, err := env.Client.Network.AddSubnet(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("type vswitch with ip range", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/add_subnet", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkActionAddSubnetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != "vswitch" { t.Errorf("unexpected Type: %v", reqBody.Type) } if reqBody.IPRange != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", reqBody.IPRange) } if reqBody.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", reqBody.NetworkZone) } if reqBody.VSwitchID != 123 { t.Errorf("unexpected VSwitchID: %v", reqBody.VSwitchID) } json.NewEncoder(w).Encode(schema.NetworkActionAddSubnetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, ipRange, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkAddSubnetOpts{ Subnet: NetworkSubnet{ Type: NetworkSubnetTypeVSwitch, IPRange: ipRange, NetworkZone: NetworkZoneEUCentral, VSwitchID: 123, }, } action, _, err := env.Client.Network.AddSubnet(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestNetworkClientDeleteSubnet(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/delete_subnet", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkActionDeleteSubnetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, ipRange, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkDeleteSubnetOpts{ Subnet: NetworkSubnet{ IPRange: ipRange, }, } action, _, err := env.Client.Network.DeleteSubnet(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientAddRoute(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/add_route", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkActionAddRouteResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, destination, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkAddRouteOpts{ Route: NetworkRoute{ Destination: destination, Gateway: net.ParseIP("10.0.1.1"), }, } action, _, err := env.Client.Network.AddRoute(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientDeleteRoute(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/delete_route", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkActionDeleteRouteResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, destination, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkDeleteRouteOpts{ Route: NetworkRoute{ Destination: destination, Gateway: net.ParseIP("10.0.1.1"), }, } action, _, err := env.Client.Network.DeleteRoute(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientChangeProtection(t *testing.T) { var ( ctx = context.Background() network = &Network{ID: 1} ) t.Run("enable delete protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.NetworkActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } json.NewEncoder(w).Encode(schema.NetworkActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := NetworkChangeProtectionOpts{ Delete: Bool(true), } action, _, err := env.Client.Network.ChangeProtection(ctx, network, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/placement_group.go000066400000000000000000000157051414442033200257070ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // PlacementGroup represents a Placement Group in the Hetzner Cloud. type PlacementGroup struct { ID int Name string Labels map[string]string Created time.Time Servers []int Type PlacementGroupType } // PlacementGroupType specifies the type of a Placement Group type PlacementGroupType string const ( // PlacementGroupTypeSpread spreads all servers in the group on different vhosts PlacementGroupTypeSpread PlacementGroupType = "spread" ) // PlacementGroupClient is a client for the Placement Groups API. type PlacementGroupClient struct { client *Client } // GetByID retrieves a PlacementGroup by its ID. If the PlacementGroup does not exist, nil is returned. func (c *PlacementGroupClient) GetByID(ctx context.Context, id int) (*PlacementGroup, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/placement_groups/%d", id), nil) if err != nil { return nil, nil, err } var body schema.PlacementGroupGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return PlacementGroupFromSchema(body.PlacementGroup), resp, nil } // GetByName retrieves a PlacementGroup by its name. If the PlacementGroup does not exist, nil is returned. func (c *PlacementGroupClient) GetByName(ctx context.Context, name string) (*PlacementGroup, *Response, error) { if name == "" { return nil, nil, nil } placementGroups, response, err := c.List(ctx, PlacementGroupListOpts{Name: name}) if len(placementGroups) == 0 { return nil, response, err } return placementGroups[0], response, err } // Get retrieves a PlacementGroup by its ID if the input can be parsed as an integer, otherwise it // retrieves a PlacementGroup by its name. If the PlacementGroup does not exist, nil is returned. func (c *PlacementGroupClient) Get(ctx context.Context, idOrName string) (*PlacementGroup, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // PlacementGroupListOpts specifies options for listing PlacementGroup. type PlacementGroupListOpts struct { ListOpts Name string Type PlacementGroupType } func (l PlacementGroupListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } if l.Type != "" { vals.Add("type", string(l.Type)) } return vals } // List returns a list of PlacementGroups 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 *PlacementGroupClient) List(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, *Response, error) { path := "/placement_groups?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.PlacementGroupListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } placementGroups := make([]*PlacementGroup, 0, len(body.PlacementGroups)) for _, g := range body.PlacementGroups { placementGroups = append(placementGroups, PlacementGroupFromSchema(g)) } return placementGroups, resp, nil } // All returns all PlacementGroups. func (c *PlacementGroupClient) All(ctx context.Context) ([]*PlacementGroup, error) { opts := PlacementGroupListOpts{ ListOpts: ListOpts{ PerPage: 50, }, } return c.AllWithOpts(ctx, opts) } // AllWithOpts returns all PlacementGroups for the given options. func (c *PlacementGroupClient) AllWithOpts(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, error) { var allPlacementGroups []*PlacementGroup err := c.client.all(func(page int) (*Response, error) { opts.Page = page placementGroups, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allPlacementGroups = append(allPlacementGroups, placementGroups...) return resp, nil }) if err != nil { return nil, err } return allPlacementGroups, nil } // PlacementGroupCreateOpts specifies options for creating a new PlacementGroup. type PlacementGroupCreateOpts struct { Name string Labels map[string]string Type PlacementGroupType } // Validate checks if options are valid func (o PlacementGroupCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } return nil } // PlacementGroupCreateResult is the result of a create PlacementGroup call. type PlacementGroupCreateResult struct { PlacementGroup *PlacementGroup Action *Action } // Create creates a new PlacementGroup func (c *PlacementGroupClient) Create(ctx context.Context, opts PlacementGroupCreateOpts) (PlacementGroupCreateResult, *Response, error) { if err := opts.Validate(); err != nil { return PlacementGroupCreateResult{}, nil, err } reqBody := placementGroupCreateOptsToSchema(opts) reqBodyData, err := json.Marshal(reqBody) if err != nil { return PlacementGroupCreateResult{}, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/placement_groups", bytes.NewReader(reqBodyData)) if err != nil { return PlacementGroupCreateResult{}, nil, err } respBody := schema.PlacementGroupCreateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return PlacementGroupCreateResult{}, nil, err } result := PlacementGroupCreateResult{ PlacementGroup: PlacementGroupFromSchema(respBody.PlacementGroup), } if respBody.Action != nil { result.Action = ActionFromSchema(*respBody.Action) } return result, resp, nil } // PlacementGroupUpdateOpts specifies options for updating a PlacementGroup. type PlacementGroupUpdateOpts struct { Name string Labels map[string]string } // Update updates a PlacementGroup. func (c *PlacementGroupClient) Update(ctx context.Context, placementGroup *PlacementGroup, opts PlacementGroupUpdateOpts) (*PlacementGroup, *Response, error) { reqBody := schema.PlacementGroupUpdateRequest{} 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("/placement_groups/%d", placementGroup.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.PlacementGroupUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return PlacementGroupFromSchema(respBody.PlacementGroup), resp, nil } // Delete deletes a PlacementGroup. func (c *PlacementGroupClient) Delete(ctx context.Context, placementGroup *PlacementGroup) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/placement_groups/%d", placementGroup.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/placement_group_test.go000066400000000000000000000155651414442033200267520ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net/http" "testing" "github.com/google/go-cmp/cmp" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestPlacementGroupClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() const id = 1 env.Mux.HandleFunc(fmt.Sprintf("/placement_groups/%d", id), func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.PlacementGroupGetResponse{ PlacementGroup: schema.PlacementGroup{ ID: id, }, }) }) checkError := func(t *testing.T, placementGroup *PlacementGroup, err error) { if err != nil { t.Fatal(err) } if placementGroup == nil { t.Fatal("no placement group") } if placementGroup.ID != id { t.Errorf("unexpected placement group ID: %v", placementGroup.ID) } } ctx := context.Background() t.Run("called via GetByID", func(t *testing.T) { placementGroup, _, err := env.Client.PlacementGroup.GetByID(ctx, 1) checkError(t, placementGroup, err) }) t.Run("called via Get", func(t *testing.T) { placementGroup, _, err := env.Client.PlacementGroup.Get(ctx, "1") checkError(t, placementGroup, err) }) } func TestPlacementGroupClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/placement_groups/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() placementGroup, _, err := env.Client.PlacementGroup.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if placementGroup != nil { t.Fatal("expected no placement_group") } } func TestPlacementGroupClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() const ( id = 1 name = "my_placement_group" ) env.Mux.HandleFunc("/placement_groups", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != fmt.Sprintf("name=%s", name) { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.PlacementGroupListResponse{ PlacementGroups: []schema.PlacementGroup{ { ID: id, Name: name, }, }, }) }) checkError := func(t *testing.T, placementGroup *PlacementGroup, err error) { if err != nil { t.Fatal(err) } if placementGroup == nil { t.Fatal("no placement group") } if placementGroup.ID != id { t.Errorf("unexpected placement group ID: %v", placementGroup.ID) } if placementGroup.Name != name { t.Errorf("unexpected placement group Name: %v", placementGroup.Name) } } ctx := context.Background() t.Run("called via GetByID", func(t *testing.T) { placementGroup, _, err := env.Client.PlacementGroup.GetByName(ctx, name) checkError(t, placementGroup, err) }) t.Run("called via Get", func(t *testing.T) { placementGroup, _, err := env.Client.PlacementGroup.Get(ctx, name) checkError(t, placementGroup, err) }) } func TestPlacementGroupClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() const name = "my_placement_group" env.Mux.HandleFunc("/placement_groups", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != fmt.Sprintf("name=%s", name) { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.PlacementGroupListResponse{ PlacementGroups: []schema.PlacementGroup{}, }) }) ctx := context.Background() placementGroup, _, err := env.Client.PlacementGroup.GetByName(ctx, name) if err != nil { t.Fatal(err) } if placementGroup != nil { t.Fatal("expected no placement_group") } } func TestPlacementGroupClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() placementGroup, _, err := env.Client.PlacementGroup.GetByName(ctx, "") if err != nil { t.Fatal(err) } if placementGroup != nil { t.Fatal("expected no placement_group") } } func TestPlacementGroupCreate(t *testing.T) { env := newTestEnv() defer env.Teardown() const id = 1 var ( ctx = context.Background() opts = PlacementGroupCreateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, Type: PlacementGroupTypeSpread, } ) env.Mux.HandleFunc("/placement_groups", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.PlacementGroupCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.PlacementGroupCreateRequest{ Name: opts.Name, Labels: &opts.Labels, Type: string(opts.Type), } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.PlacementGroupCreateResponse{ PlacementGroup: schema.PlacementGroup{ ID: id, }, }) }) createdPlacementGroup, _, err := env.Client.PlacementGroup.Create(ctx, opts) if err != nil { t.Fatal(err) } if createdPlacementGroup.PlacementGroup == nil { t.Fatal("no placement group") } if createdPlacementGroup.PlacementGroup.ID != id { t.Errorf("unexpected placement group ID: %v", createdPlacementGroup.PlacementGroup.ID) } } func TestPlacementGroupDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() const id = 1 env.Mux.HandleFunc(fmt.Sprintf("/placement_groups/%d", id), func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() placementGroup = &PlacementGroup{ID: id} ) _, err := env.Client.PlacementGroup.Delete(ctx, placementGroup) if err != nil { t.Fatal(err) } } func TestPlacementGroupUpdate(t *testing.T) { env := newTestEnv() defer env.Teardown() const id = 1 var ( ctx = context.Background() placementGroup = &PlacementGroup{ID: id} opts = PlacementGroupUpdateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, } ) env.Mux.HandleFunc("/placement_groups/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.PlacementGroupUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } expectedReqBody := schema.PlacementGroupUpdateRequest{ Name: &opts.Name, Labels: &opts.Labels, } if !cmp.Equal(expectedReqBody, reqBody) { t.Log(cmp.Diff(expectedReqBody, reqBody)) t.Error("unexpected request body") } json.NewEncoder(w).Encode(schema.PlacementGroupUpdateResponse{ PlacementGroup: schema.PlacementGroup{ ID: id, }, }) }) updatedPlacementGroup, _, err := env.Client.PlacementGroup.Update(ctx, placementGroup, opts) if err != nil { t.Fatal(err) } if updatedPlacementGroup == nil { t.Fatal("no placement group") } if updatedPlacementGroup.ID != id { t.Errorf("unexpected placement group ID: %v", updatedPlacementGroup.ID) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/pricing.go000066400000000000000000000056541414442033200241600ustar00rootroot00000000000000package hcloud import ( "context" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Pricing specifies pricing information for various resources. type Pricing struct { Image ImagePricing FloatingIP FloatingIPPricing FloatingIPs []FloatingIPTypePricing Traffic TrafficPricing ServerBackup ServerBackupPricing ServerTypes []ServerTypePricing LoadBalancerTypes []LoadBalancerTypePricing Volume VolumePricing } // 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 } // FloatingIPTypePricing provides pricing information for Floating IPs per Type. type FloatingIPTypePricing struct { Type FloatingIPType Pricings []FloatingIPTypeLocationPricing } // FloatingIPTypeLocationPricing provides pricing information for a Floating IP type // at a location. type FloatingIPTypeLocationPricing struct { Location *Location Monthly Price } // TrafficPricing provides pricing information for traffic. type TrafficPricing struct { PerTB Price } // VolumePricing provides pricing information for a Volume. type VolumePricing struct { PerGBMonthly 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/pricing_test.go000066400000000000000000000012171414442033200252060ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestPricingClientGet(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/pricing", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.PricingGetResponse{ Pricing: schema.Pricing{ Currency: "EUR", }, }) }) ctx := context.Background() pricing, _, err := env.Client.Pricing.Get(ctx) if err != nil { t.Fatal(err) } if pricing.Image.PerGBMonth.Currency != "EUR" { t.Errorf("unexpected currency: %v", pricing.Image.PerGBMonth.Currency) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/rdns.go000066400000000000000000000031741414442033200234660ustar00rootroot00000000000000package hcloud import ( "context" "fmt" "net" ) // RDNSSupporter defines functions to change and lookup reverse dns entries. // currently implemented by Server, FloatingIP and LoadBalancer type RDNSSupporter interface { // changeDNSPtr changes or resets the reverse DNS pointer for a IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) // GetDNSPtrForIP searches for the dns assigned to the given IP address. // It returns an error if there is no dns set for the given IP address. GetDNSPtrForIP(ip net.IP) (string, error) } // RDNSClient simplifys the handling objects which support reverse dns entries. type RDNSClient struct { client *Client } // ChangeDNSPtr changes or resets the reverse DNS pointer for a IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (c *RDNSClient) ChangeDNSPtr(ctx context.Context, rdns RDNSSupporter, ip net.IP, ptr *string) (*Action, *Response, error) { return rdns.changeDNSPtr(ctx, c.client, ip, ptr) } // SupportsRDNS checks if the object supports reverse dns functions. func SupportsRDNS(i interface{}) bool { _, ok := i.(RDNSSupporter) return ok } // RDNSLookup searches for the dns assigned to the given IP address. // It returns an error if the object does not support reverse dns or if there is no dns set for the given IP address. func RDNSLookup(i interface{}, ip net.IP) (string, error) { rdns, ok := i.(RDNSSupporter) if !ok { return "", fmt.Errorf("%+v does not support RDNS", i) } return rdns.GetDNSPtrForIP(ip) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/rdns_test.go000066400000000000000000000164401414442033200245250ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestChangeDNSPtr(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} loadBalancer = &LoadBalancer{ID: 1} floatingIP = &FloatingIP{ID: 1} dns = "example.com" ) tests := []struct { name string apiURL string IP net.IP DNS *string changeFunc func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) }{ { name: "set via server client", apiURL: "/servers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: &dns, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.Server.ChangeDNSPtr(ctx, server, ip.String(), dns) }, }, { name: "reset via server client", apiURL: "/servers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: nil, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.Server.ChangeDNSPtr(ctx, server, ip.String(), dns) }, }, { name: "set server via rdns client", apiURL: "/servers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: &dns, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.RDNS.ChangeDNSPtr(ctx, server, ip, dns) }, }, { name: "reset server via rdns client", apiURL: "/servers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: nil, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.RDNS.ChangeDNSPtr(ctx, server, ip, dns) }, }, { name: "set via load balancer client", apiURL: "/load_balancers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: &dns, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.LoadBalancer.ChangeDNSPtr(ctx, loadBalancer, ip.String(), dns) }, }, { name: "reset via load balancer client", apiURL: "/load_balancers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: nil, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.LoadBalancer.ChangeDNSPtr(ctx, loadBalancer, ip.String(), dns) }, }, { name: "set load balancer via rdns client", apiURL: "/load_balancers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: &dns, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.RDNS.ChangeDNSPtr(ctx, loadBalancer, ip, dns) }, }, { name: "reset load balancer via rdns client", apiURL: "/load_balancers/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: nil, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.RDNS.ChangeDNSPtr(ctx, loadBalancer, ip, dns) }, }, { name: "set via floating ip client", apiURL: "/floating_ips/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: &dns, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, ip.String(), dns) }, }, { name: "reset via floating ip client", apiURL: "/floating_ips/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: nil, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, ip.String(), dns) }, }, { name: "set floating ip via rdns client", apiURL: "/floating_ips/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: &dns, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.RDNS.ChangeDNSPtr(ctx, floatingIP, ip, dns) }, }, { name: "reset floating ip via rdns client", apiURL: "/floating_ips/1/actions/change_dns_ptr", IP: net.ParseIP("127.0.0.1"), DNS: nil, changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) { return env.Client.RDNS.ChangeDNSPtr(ctx, floatingIP, ip, dns) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc(tt.apiURL, func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionChangeDNSPtrRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.IP != tt.IP.String() { t.Errorf("unexpected IP: %v", reqBody.IP) } if !(reqBody.DNSPtr == tt.DNS || *reqBody.DNSPtr == *tt.DNS) { t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr) } json.NewEncoder(w).Encode(schema.ServerActionChangeDNSPtrResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := tt.changeFunc(&env, tt.IP, tt.DNS) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } } func TestDNSPtrFromIP(t *testing.T) { var ( server = Server{ ID: 1, PublicNet: ServerPublicNet{ IPv4: ServerPublicNetIPv4{ IP: net.ParseIP("127.0.0.1"), DNSPtr: "ipv4.example.com", }, IPv6: ServerPublicNetIPv6{ DNSPtr: map[string]string{ "::1": "ipv6.example.com", }, }, }, } loadBalancer = LoadBalancer{ ID: 1, PublicNet: LoadBalancerPublicNet{ IPv4: LoadBalancerPublicNetIPv4{ IP: net.ParseIP("127.0.0.1"), DNSPtr: "ipv4.example.com", }, IPv6: LoadBalancerPublicNetIPv6{ IP: net.ParseIP("::1"), DNSPtr: "ipv6.example.com", }, }, } floatingIPv4 = FloatingIP{ ID: 1, IP: net.ParseIP("127.0.0.1"), DNSPtr: map[string]string{ "127.0.0.1": "ipv4.example.com", }, } floatintIPv6 = FloatingIP{ ID: 1, IP: net.ParseIP("::1"), DNSPtr: map[string]string{ "::1": "ipv6.example.com", }, } ) tests := []struct { name string IP net.IP DNS string rdns RDNSSupporter }{ { name: "server get dns ptr of IPv4", IP: net.ParseIP("127.0.0.1"), DNS: "ipv4.example.com", rdns: &server, }, { name: "server get dns ptr of IPv6", IP: net.ParseIP("::1"), DNS: "ipv6.example.com", rdns: &server, }, { name: "load balancer get dns ptr of IPv4", IP: net.ParseIP("127.0.0.1"), DNS: "ipv4.example.com", rdns: &loadBalancer, }, { name: "load balancer get dns ptr of IPv6", IP: net.ParseIP("::1"), DNS: "ipv6.example.com", rdns: &loadBalancer, }, { name: "floating ip get dns ptr of IPv4", IP: net.ParseIP("127.0.0.1"), DNS: "ipv4.example.com", rdns: &floatingIPv4, }, { name: "floating ip get dns ptr of IPv6", IP: net.ParseIP("::1"), DNS: "ipv6.example.com", rdns: &floatintIPv6, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { receivedDNS, err := tt.rdns.GetDNSPtrForIP(tt.IP) if err != nil { t.Fatal(err) } if tt.DNS != receivedDNS { t.Errorf("unexpected dns for ip %s: %s", tt.IP.String(), receivedDNS) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/resource.go000066400000000000000000000001561414442033200243440ustar00rootroot00000000000000package hcloud // Resource defines the schema of a resource. type Resource struct { ID int Type string } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema.go000066400000000000000000001043261414442033200237610ustar00rootroot00000000000000package hcloud import ( "fmt" "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 { actions := make([]*Action, len(s)) for i, a := range s { actions[i] = 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, PrimaryDiskSize: s.PrimaryDiskSize, 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)) } if s.PlacementGroup != nil { server.PlacementGroup = PlacementGroupFromSchema(*s.PlacementGroup) } 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}) } for _, fw := range s.Firewalls { publicNet.Firewalls = append(publicNet.Firewalls, &ServerFirewallStatus{ Firewall: Firewall{ID: fw.ID}, Status: FirewallStatus(fw.Status)}, ) } 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, Deleted: s.Deleted, } 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), VSwitchID: s.VSwitchID, } _, 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), DNSPtr: s.PublicNet.IPv4.DNSPtr, }, IPv6: LoadBalancerPublicNetIPv6{ IP: net.ParseIP(s.PublicNet.IPv6.IP), DNSPtr: s.PublicNet.IPv6.DNSPtr, }, }, 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, Type: CertificateType(s.Type), Certificate: s.Certificate, Created: s.Created, NotValidBefore: s.NotValidBefore, NotValidAfter: s.NotValidAfter, DomainNames: s.DomainNames, Fingerprint: s.Fingerprint, } if s.Status != nil { c.Status = &CertificateStatus{ Issuance: CertificateStatusType(s.Status.Issuance), Renewal: CertificateStatusType(s.Status.Renewal), } if s.Status.Error != nil { certErr := ErrorFromSchema(*s.Status.Error) c.Status.Error = &certErr } } if len(s.Labels) > 0 { c.Labels = s.Labels } if len(s.UsedBy) > 0 { c.UsedBy = make([]CertificateUsedByRef, len(s.UsedBy)) for i, ref := range s.UsedBy { c.UsedBy[i] = CertificateUsedByRef{ID: ref.ID, Type: CertificateUsedByRefType(ref.Type)} } } 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, } if d, ok := s.Details.(schema.ErrorDetailsInvalidInput); ok { 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, }, Volume: VolumePricing{ PerGBMonthly: Price{ Currency: s.Currency, VATRate: s.VATRate, Net: s.Volume.PricePerGBPerMonth.Net, Gross: s.Volume.PricePerGBPerMonth.Gross, }, }, } for _, floatingIPType := range s.FloatingIPs { var pricings []FloatingIPTypeLocationPricing for _, price := range floatingIPType.Prices { p := FloatingIPTypeLocationPricing{ Location: &Location{Name: price.Location}, Monthly: Price{ Currency: s.Currency, VATRate: s.VATRate, Net: price.PriceMonthly.Net, Gross: price.PriceMonthly.Gross, }, } pricings = append(pricings, p) } p.FloatingIPs = append(p.FloatingIPs, FloatingIPTypePricing{Type: FloatingIPType(floatingIPType.Type), Pricings: pricings}) } 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 } // FirewallFromSchema converts a schema.Firewall to a Firewall. func FirewallFromSchema(s schema.Firewall) *Firewall { f := &Firewall{ ID: s.ID, Name: s.Name, Labels: map[string]string{}, Created: s.Created, } for key, value := range s.Labels { f.Labels[key] = value } for _, res := range s.AppliedTo { r := FirewallResource{Type: FirewallResourceType(res.Type)} switch r.Type { case FirewallResourceTypeLabelSelector: r.LabelSelector = &FirewallResourceLabelSelector{Selector: res.LabelSelector.Selector} case FirewallResourceTypeServer: r.Server = &FirewallResourceServer{ID: res.Server.ID} } f.AppliedTo = append(f.AppliedTo, r) } for _, rule := range s.Rules { sourceIPs := []net.IPNet{} for _, sourceIP := range rule.SourceIPs { _, mask, err := net.ParseCIDR(sourceIP) if err == nil && mask != nil { sourceIPs = append(sourceIPs, *mask) } } destinationIPs := []net.IPNet{} for _, destinationIP := range rule.DestinationIPs { _, mask, err := net.ParseCIDR(destinationIP) if err == nil && mask != nil { destinationIPs = append(destinationIPs, *mask) } } f.Rules = append(f.Rules, FirewallRule{ Direction: FirewallRuleDirection(rule.Direction), SourceIPs: sourceIPs, DestinationIPs: destinationIPs, Protocol: FirewallRuleProtocol(rule.Protocol), Port: rule.Port, Description: rule.Description, }) } return f } // PlacementGroupFromSchema converts a schema.PlacementGroup to a PlacementGroup. func PlacementGroupFromSchema(s schema.PlacementGroup) *PlacementGroup { g := &PlacementGroup{ ID: s.ID, Name: s.Name, Labels: s.Labels, Created: s.Created, Servers: s.Servers, Type: PlacementGroupType(s.Type), } return g } func placementGroupCreateOptsToSchema(opts PlacementGroupCreateOpts) schema.PlacementGroupCreateRequest { req := schema.PlacementGroupCreateRequest{ Name: opts.Name, Type: string(opts.Type), } if opts.Labels != nil { req.Labels = &opts.Labels } return req } 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 service.HTTP.CookieLifetime != nil { 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 } func firewallCreateOptsToSchema(opts FirewallCreateOpts) schema.FirewallCreateRequest { req := schema.FirewallCreateRequest{ Name: opts.Name, } if opts.Labels != nil { req.Labels = &opts.Labels } for _, rule := range opts.Rules { schemaRule := schema.FirewallRule{ Direction: string(rule.Direction), Protocol: string(rule.Protocol), Port: rule.Port, Description: rule.Description, } switch rule.Direction { case FirewallRuleDirectionOut: schemaRule.DestinationIPs = make([]string, len(rule.DestinationIPs)) for i, destinationIP := range rule.DestinationIPs { schemaRule.DestinationIPs[i] = destinationIP.String() } case FirewallRuleDirectionIn: schemaRule.SourceIPs = make([]string, len(rule.SourceIPs)) for i, sourceIP := range rule.SourceIPs { schemaRule.SourceIPs[i] = sourceIP.String() } } req.Rules = append(req.Rules, schemaRule) } for _, res := range opts.ApplyTo { schemaFirewallResource := schema.FirewallResource{ Type: string(res.Type), } switch res.Type { case FirewallResourceTypeServer: schemaFirewallResource.Server = &schema.FirewallResourceServer{ ID: res.Server.ID, } case FirewallResourceTypeLabelSelector: schemaFirewallResource.LabelSelector = &schema.FirewallResourceLabelSelector{Selector: res.LabelSelector.Selector} } req.ApplyTo = append(req.ApplyTo, schemaFirewallResource) } return req } func firewallSetRulesOptsToSchema(opts FirewallSetRulesOpts) schema.FirewallActionSetRulesRequest { req := schema.FirewallActionSetRulesRequest{Rules: []schema.FirewallRule{}} for _, rule := range opts.Rules { schemaRule := schema.FirewallRule{ Direction: string(rule.Direction), Protocol: string(rule.Protocol), Port: rule.Port, Description: rule.Description, } switch rule.Direction { case FirewallRuleDirectionOut: schemaRule.DestinationIPs = make([]string, len(rule.DestinationIPs)) for i, destinationIP := range rule.DestinationIPs { schemaRule.DestinationIPs[i] = destinationIP.String() } case FirewallRuleDirectionIn: schemaRule.SourceIPs = make([]string, len(rule.SourceIPs)) for i, sourceIP := range rule.SourceIPs { schemaRule.SourceIPs[i] = sourceIP.String() } } req.Rules = append(req.Rules, schemaRule) } return req } func firewallResourceToSchema(resource FirewallResource) schema.FirewallResource { s := schema.FirewallResource{ Type: string(resource.Type), } switch resource.Type { case FirewallResourceTypeLabelSelector: s.LabelSelector = &schema.FirewallResourceLabelSelector{Selector: resource.LabelSelector.Selector} case FirewallResourceTypeServer: s.Server = &schema.FirewallResourceServer{ID: resource.Server.ID} } return s } func serverMetricsFromSchema(s *schema.ServerGetMetricsResponse) (*ServerMetrics, error) { ms := ServerMetrics{ Start: s.Metrics.Start, End: s.Metrics.End, Step: s.Metrics.Step, } timeSeries := make(map[string][]ServerMetricsValue) for tsName, v := range s.Metrics.TimeSeries { vals := make([]ServerMetricsValue, len(v.Values)) for i, rawVal := range v.Values { var val ServerMetricsValue tup, ok := rawVal.([]interface{}) if !ok { return nil, fmt.Errorf("failed to convert value to tuple: %v", rawVal) } if len(tup) != 2 { return nil, fmt.Errorf("invalid tuple size: %d: %v", len(tup), rawVal) } ts, ok := tup[0].(float64) if !ok { return nil, fmt.Errorf("convert to float64: %v", tup[0]) } val.Timestamp = ts v, ok := tup[1].(string) if !ok { return nil, fmt.Errorf("not a string: %v", tup[1]) } val.Value = v vals[i] = val } timeSeries[tsName] = vals } ms.TimeSeries = timeSeries return &ms, nil } func loadBalancerMetricsFromSchema(s *schema.LoadBalancerGetMetricsResponse) (*LoadBalancerMetrics, error) { ms := LoadBalancerMetrics{ Start: s.Metrics.Start, End: s.Metrics.End, Step: s.Metrics.Step, } timeSeries := make(map[string][]LoadBalancerMetricsValue) for tsName, v := range s.Metrics.TimeSeries { vals := make([]LoadBalancerMetricsValue, len(v.Values)) for i, rawVal := range v.Values { var val LoadBalancerMetricsValue tup, ok := rawVal.([]interface{}) if !ok { return nil, fmt.Errorf("failed to convert value to tuple: %v", rawVal) } if len(tup) != 2 { return nil, fmt.Errorf("invalid tuple size: %d: %v", len(tup), rawVal) } ts, ok := tup[0].(float64) if !ok { return nil, fmt.Errorf("convert to float64: %v", tup[0]) } val.Timestamp = ts v, ok := tup[1].(string) if !ok { return nil, fmt.Errorf("not a string: %v", tup[1]) } val.Value = v vals[i] = val } timeSeries[tsName] = vals } ms.TimeSeries = timeSeries return &ms, nil } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/000077500000000000000000000000001414442033200234245ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/action.go000066400000000000000000000022141414442033200252270ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/certificate.go000066400000000000000000000053401414442033200262370ustar00rootroot00000000000000package schema import "time" // CertificateUsedByRef defines the schema of a resource using a certificate. type CertificateUsedByRef struct { ID int `json:"id"` Type string `json:"type"` } type CertificateStatusRef struct { Issuance string `json:"issuance"` Renewal string `json:"renewal"` Error *Error `json:"error,omitempty"` } // Certificate defines the schema of an certificate. type Certificate struct { ID int `json:"id"` Name string `json:"name"` Labels map[string]string `json:"labels"` Type string `json:"type"` 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"` Status *CertificateStatusRef `json:"status"` UsedBy []CertificateUsedByRef `json:"used_by"` } // 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"` Type string `json:"type"` DomainNames []string `json:"domain_names,omitempty"` Certificate string `json:"certificate,omitempty"` PrivateKey string `json:"private_key,omitempty"` 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"` Action *Action `json:"action"` } // 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"` } // CertificateIssuanceRetryResponse defines the schema for the response of the // retry issuance endpoint. type CertificateIssuanceRetryResponse struct { Action Action `json:"action"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/datacenter.go000066400000000000000000000013201414442033200260610ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/error.go000066400000000000000000000020551414442033200251060ustar00rootroot00000000000000package 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 } if e.Code == "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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/error_test.go000066400000000000000000000025171414442033200261500ustar00rootroot00000000000000package schema import ( "encoding/json" "testing" ) func TestError(t *testing.T) { t.Run("UnmarshalJSON", func(t *testing.T) { data := []byte(`{ "code": "invalid_input", "message": "invalid input", "details": { "fields": [ { "name": "broken_field", "messages": ["is required"] } ] } }`) e := &Error{} err := json.Unmarshal(data, e) if err != nil { t.Fatalf("unexpected error: %v", err) } if e.Code != "invalid_input" { t.Errorf("unexpected Code: %v", e.Code) } if e.Message != "invalid input" { t.Errorf("unexpected Message: %v", e.Message) } if e.Details == nil { t.Fatalf("unexpected Details: %v", e.Details) } d, ok := e.Details.(ErrorDetailsInvalidInput) if !ok { t.Fatalf("unexpected Details type (should be ErrorDetailsInvalidInput): %v", e.Details) } if len(d.Fields) != 1 { t.Fatalf("unexpected Details.Fields length (should be 1): %v", d.Fields) } if d.Fields[0].Name != "broken_field" { t.Errorf("unexpected Details.Fields[0].Name: %v", d.Fields[0].Name) } if len(d.Fields[0].Messages) != 1 { t.Fatalf("unexpected Details.Fields[0].Messages length (should be 1): %v", d.Fields[0].Messages) } if d.Fields[0].Messages[0] != "is required" { t.Errorf("unexpected Details.Fields[0].Messages[0]: %v", d.Fields[0].Messages[0]) } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/firewall.go000066400000000000000000000074541414442033200255720ustar00rootroot00000000000000package schema import "time" // Firewall defines the schema of a Firewall. type Firewall struct { ID int `json:"id"` Name string `json:"name"` Labels map[string]string `json:"labels"` Created time.Time `json:"created"` Rules []FirewallRule `json:"rules"` AppliedTo []FirewallResource `json:"applied_to"` } // FirewallRule defines the schema of a Firewall rule. type FirewallRule struct { Direction string `json:"direction"` SourceIPs []string `json:"source_ips,omitempty"` DestinationIPs []string `json:"destination_ips,omitempty"` Protocol string `json:"protocol"` Port *string `json:"port,omitempty"` Description *string `json:"description,omitempty"` } // FirewallListResponse defines the schema of the response when listing Firewalls. type FirewallListResponse struct { Firewalls []Firewall `json:"firewalls"` } // FirewallGetResponse defines the schema of the response when retrieving a single Firewall. type FirewallGetResponse struct { Firewall Firewall `json:"firewall"` } // FirewallCreateRequest defines the schema of the request to create a Firewall. type FirewallCreateRequest struct { Name string `json:"name"` Labels *map[string]string `json:"labels,omitempty"` Rules []FirewallRule `json:"rules,omitempty"` ApplyTo []FirewallResource `json:"apply_to,omitempty"` } // FirewallResource defines the schema of a resource to apply the new Firewall on. type FirewallResource struct { Type string `json:"type"` Server *FirewallResourceServer `json:"server,omitempty"` LabelSelector *FirewallResourceLabelSelector `json:"label_selector,omitempty"` } // FirewallResourceLabelSelector defines the schema of a LabelSelector to apply a Firewall on. type FirewallResourceLabelSelector struct { Selector string `json:"selector"` } // FirewallResourceServer defines the schema of a Server to apply a Firewall on. type FirewallResourceServer struct { ID int `json:"id"` } // FirewallCreateResponse defines the schema of the response when creating a Firewall. type FirewallCreateResponse struct { Firewall Firewall `json:"firewall"` Actions []Action `json:"actions"` } // FirewallUpdateRequest defines the schema of the request to update a Firewall. type FirewallUpdateRequest struct { Name *string `json:"name,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // FirewallUpdateResponse defines the schema of the response when updating a Firewall. type FirewallUpdateResponse struct { Firewall Firewall `json:"firewall"` } // FirewallActionSetRulesRequest defines the schema of the request when setting Firewall rules. type FirewallActionSetRulesRequest struct { Rules []FirewallRule `json:"rules"` } // FirewallActionSetRulesResponse defines the schema of the response when setting Firewall rules. type FirewallActionSetRulesResponse struct { Actions []Action `json:"actions"` } // FirewallActionApplyToResourcesRequest defines the schema of the request when applying a Firewall on resources. type FirewallActionApplyToResourcesRequest struct { ApplyTo []FirewallResource `json:"apply_to"` } // FirewallActionApplyToResourcesResponse defines the schema of the response when applying a Firewall on resources. type FirewallActionApplyToResourcesResponse struct { Actions []Action `json:"actions"` } // FirewallActionRemoveFromResourcesRequest defines the schema of the request when removing a Firewall from resources. type FirewallActionRemoveFromResourcesRequest struct { RemoveFrom []FirewallResource `json:"remove_from"` } // FirewallActionRemoveFromResourcesResponse defines the schema of the response when removing a Firewall from resources. type FirewallActionRemoveFromResourcesResponse struct { Actions []Action `json:"actions"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/floating_ip.go000066400000000000000000000101711414442033200262460ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/floating_ip_test.go000066400000000000000000000022501414442033200273040ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestFloatingIPCreateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in FloatingIPCreateRequest out []byte }{ { name: "no labels", in: FloatingIPCreateRequest{Type: "ipv4"}, out: []byte(`{"type":"ipv4"}`), }, { name: "one label", in: FloatingIPCreateRequest{Type: "ipv4", Labels: &oneLabel}, out: []byte(`{"type":"ipv4","labels":{"foo":"bar"}}`), }, { name: "nil labels", in: FloatingIPCreateRequest{Type: "ipv4", Labels: &nilLabels}, out: []byte(`{"type":"ipv4","labels":null}`), }, { name: "empty labels", in: FloatingIPCreateRequest{Type: "ipv4", Labels: &emptyLabels}, out: []byte(`{"type":"ipv4","labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/image.go000066400000000000000000000045031414442033200250370ustar00rootroot00000000000000package 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"` Deleted time.Time `json:"deleted"` 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/image_test.go000066400000000000000000000020351414442033200260740ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestImageUpdateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in ImageUpdateRequest out []byte }{ { name: "no labels", in: ImageUpdateRequest{}, out: []byte(`{}`), }, { name: "one label", in: ImageUpdateRequest{Labels: &oneLabel}, out: []byte(`{"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: ImageUpdateRequest{Labels: &nilLabels}, out: []byte(`{"labels":null}`), }, { name: "empty labels", in: ImageUpdateRequest{Labels: &emptyLabels}, out: []byte(`{"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/iso.go000066400000000000000000000010571414442033200245500ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/load_balancer.go000066400000000000000000000372701414442033200265320ustar00rootroot00000000000000package 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"` DNSPtr string `json:"dns_ptr"` } type LoadBalancerPublicNetIPv6 struct { IP string `json:"ip"` DNSPtr string `json:"dns_ptr"` } 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"` } // LoadBalancerGetMetricsResponse defines the schema of the response when // requesting metrics for a Load Balancer. type LoadBalancerGetMetricsResponse struct { Metrics struct { Start time.Time `json:"start"` End time.Time `json:"end"` Step float64 `json:"step"` TimeSeries map[string]LoadBalancerTimeSeriesVals `json:"time_series"` } `json:"metrics"` } // LoadBalancerTimeSeriesVals contains the values for a Load Balancer time // series. type LoadBalancerTimeSeriesVals struct { Values []interface{} `json:"values"` } // LoadBalancerActionChangeDNSPtrRequest defines the schema for the request to // change a Load Balancer reverse DNS pointer. type LoadBalancerActionChangeDNSPtrRequest struct { IP string `json:"ip"` DNSPtr *string `json:"dns_ptr"` } // LoadBalancerActionChangeDNSPtrResponse defines the schema of the response when // creating a change_dns_ptr Floating IP action. type LoadBalancerActionChangeDNSPtrResponse struct { Action Action `json:"action"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/load_balancer_type.go000066400000000000000000000022131414442033200275600ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/location.go000066400000000000000000000013211414442033200255600ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/meta.go000066400000000000000000000011631414442033200247020ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/network.go000066400000000000000000000114731414442033200254520ustar00rootroot00000000000000package 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,omitempty"` VSwitchID int `json:"vswitch_id,omitempty"` } // 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"` VSwitchID int `json:"vswitch_id,omitempty"` } // 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/network_test.go000066400000000000000000000020511414442033200265010ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestNetworkUpdateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in NetworkUpdateRequest out []byte }{ { name: "no labels", in: NetworkUpdateRequest{}, out: []byte(`{}`), }, { name: "one label", in: NetworkUpdateRequest{Labels: &oneLabel}, out: []byte(`{"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: NetworkUpdateRequest{Labels: &nilLabels}, out: []byte(`{"labels":null}`), }, { name: "empty labels", in: NetworkUpdateRequest{Labels: &emptyLabels}, out: []byte(`{"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/placement_group.go000066400000000000000000000021111414442033200271320ustar00rootroot00000000000000package schema import "time" type PlacementGroup struct { ID int `json:"id"` Name string `json:"name"` Labels map[string]string `json:"labels"` Created time.Time `json:"created"` Servers []int `json:"servers"` Type string `json:"type"` } type PlacementGroupListResponse struct { PlacementGroups []PlacementGroup `json:"placement_groups"` } type PlacementGroupGetResponse struct { PlacementGroup PlacementGroup `json:"placement_group"` } type PlacementGroupCreateRequest struct { Name string `json:"name"` Labels *map[string]string `json:"labels,omitempty"` Type string `json:"type"` } type PlacementGroupCreateResponse struct { PlacementGroup PlacementGroup `json:"placement_group"` Action *Action `json:"action"` } type PlacementGroupUpdateRequest struct { Name *string `json:"name,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } type PlacementGroupUpdateResponse struct { PlacementGroup PlacementGroup `json:"placement_group"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/pricing.go000066400000000000000000000065451414442033200254200ustar00rootroot00000000000000package 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"` FloatingIPs []PricingFloatingIPType `json:"floating_ips"` Traffic PricingTraffic `json:"traffic"` ServerBackup PricingServerBackup `json:"server_backup"` ServerTypes []PricingServerType `json:"server_types"` LoadBalancerTypes []PricingLoadBalancerType `json:"load_balancer_types"` Volume PricingVolume `json:"volume"` } // 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"` } // PricingFloatingIPType defines the schema of pricing information for a Floating IP per type. type PricingFloatingIPType struct { Type string `json:"type"` Prices []PricingFloatingIPTypePrice `json:"prices"` } // PricingFloatingIPTypePrice defines the schema of pricing information for a Floating IP // type at a location. type PricingFloatingIPTypePrice struct { Location string `json:"location"` PriceMonthly Price `json:"price_monthly"` } // PricingTraffic defines the schema of pricing information for traffic. type PricingTraffic struct { PricePerTB Price `json:"price_per_tb"` } // PricingVolume defines the schema of pricing information for a Volume. type PricingVolume struct { PricePerGBPerMonth Price `json:"price_per_gb_month"` } // 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/server.go000066400000000000000000000351041414442033200252640ustar00rootroot00000000000000package 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"` PrimaryDiskSize int `json:"primary_disk_size"` PlacementGroup *PlacementGroup `json:"placement_group"` } // 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"` Firewalls []ServerFirewall `json:"firewalls"` } // 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"` } // ServerFirewall defines the schema of a Server's Firewalls on // a certain network interface. type ServerFirewall struct { ID int `json:"id"` Status string `json:"status"` } // 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"` Firewalls []ServerCreateFirewalls `json:"firewalls,omitempty"` PlacementGroup int `json:"placement_group,omitempty"` } // ServerCreateFirewall defines which Firewalls to apply when creating a Server. type ServerCreateFirewalls struct { Firewall int `json:"firewall"` } // 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"` } // ServerGetMetricsResponse defines the schema of the response when requesting // metrics for a server. type ServerGetMetricsResponse struct { Metrics struct { Start time.Time `json:"start"` End time.Time `json:"end"` Step float64 `json:"step"` TimeSeries map[string]ServerTimeSeriesVals `json:"time_series"` } `json:"metrics"` } // ServerTimeSeriesVals contains the values for a Server time series. type ServerTimeSeriesVals struct { Values []interface{} `json:"values"` } // ServerActionAddToPlacementGroupRequest defines the schema for the request to // add a server to a placement group. type ServerActionAddToPlacementGroupRequest struct { PlacementGroup int `json:"placement_group"` } // ServerActionAddToPlacementGroupResponse defines the schema of the response when // creating an add_to_placement_group server action. type ServerActionAddToPlacementGroupResponse struct { Action Action `json:"action"` } // ServerActionRemoveFromPlacementGroupResponse defines the schema of the response when // creating a remove_from_placement_group server action. type ServerActionRemoveFromPlacementGroupResponse struct { Action Action `json:"action"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/server_test.go000066400000000000000000000023401414442033200263170ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestServerActionCreateImageRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in ServerActionCreateImageRequest out []byte }{ { name: "no labels", in: ServerActionCreateImageRequest{}, out: []byte(`{"type":null,"description":null}`), }, { name: "one label", in: ServerActionCreateImageRequest{Labels: &oneLabel}, out: []byte(`{"type":null,"description":null,"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: ServerActionCreateImageRequest{Labels: &nilLabels}, out: []byte(`{"type":null,"description":null,"labels":null}`), }, { name: "empty labels", in: ServerActionCreateImageRequest{Labels: &emptyLabels}, out: []byte(`{"type":null,"description":null,"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/server_type.go000066400000000000000000000016631414442033200263300ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/ssh_key.go000066400000000000000000000027301414442033200254220ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/ssh_key_test.go000066400000000000000000000024441414442033200264630ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestSSHKeyCreateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in SSHKeyCreateRequest out []byte }{ { name: "no labels", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key"}, out: []byte(`{"name":"test","public_key":"key"}`), }, { name: "one label", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &oneLabel}, out: []byte(`{"name":"test","public_key":"key","labels":{"foo":"bar"}}`), }, { name: "nil labels", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &nilLabels}, out: []byte(`{"name":"test","public_key":"key","labels":null}`), }, { name: "empty labels", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &emptyLabels}, out: []byte(`{"name":"test","public_key":"key","labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/volume.go000066400000000000000000000071461414442033200252720ustar00rootroot00000000000000package 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"` } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema/volume_test.go000066400000000000000000000020431414442033200263200ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestVolumeUpdateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in VolumeUpdateRequest out []byte }{ { name: "no labels", in: VolumeUpdateRequest{}, out: []byte(`{}`), }, { name: "one label", in: VolumeUpdateRequest{Labels: &oneLabel}, out: []byte(`{"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: VolumeUpdateRequest{Labels: &nilLabels}, out: []byte(`{"labels":null}`), }, { name: "empty labels", in: VolumeUpdateRequest{Labels: &emptyLabels}, out: []byte(`{"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/schema_test.go000066400000000000000000002430471414442033200250240ustar00rootroot00000000000000package hcloud import ( "encoding/json" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestActionFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "command": "create_server", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00Z", "finished": "2016-01-30T23:56:13Z", "resources": [ { "id": 42, "type": "server" } ], "error": { "code": "action_failed", "message": "Action failed" } }`) var s schema.Action if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } action := ActionFromSchema(s) if action.ID != 1 { t.Errorf("unexpected ID: %v", action.ID) } if action.Command != "create_server" { t.Errorf("unexpected command: %v", action.Command) } if action.Status != "success" { t.Errorf("unexpected status: %v", action.Status) } if action.Progress != 100 { t.Errorf("unexpected progress: %d", action.Progress) } if !action.Started.Equal(time.Date(2016, 1, 30, 23, 55, 0, 0, time.UTC)) { t.Errorf("unexpected started: %v", action.Started) } if !action.Finished.Equal(time.Date(2016, 1, 30, 23, 56, 13, 0, time.UTC)) { t.Errorf("unexpected finished: %v", action.Started) } if action.ErrorCode != "action_failed" { t.Errorf("unexpected error code: %v", action.ErrorCode) } if action.ErrorMessage != "Action failed" { t.Errorf("unexpected error message: %v", action.ErrorMessage) } if len(action.Resources) == 1 { if action.Resources[0].ID != 42 { t.Errorf("unexpected id in resources[0].ID: %v", action.Resources[0].ID) } if action.Resources[0].Type != ActionResourceTypeServer { t.Errorf("unexpected type in resources[0].Type: %v", action.Resources[0].Type) } } else { t.Errorf("unexpected number of resources") } } func TestActionsFromSchema(t *testing.T) { data := []byte(`[ { "id": 13, "command": "create_server" }, { "id": 14, "command": "start_server" } ]`) var s []schema.Action if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } actions := ActionsFromSchema(s) if len(actions) != 2 || actions[0].ID != 13 || actions[1].ID != 14 { t.Fatal("unexpected actions") } } func TestFloatingIPFromSchema(t *testing.T) { t.Run("IPv6", func(t *testing.T) { data := []byte(`{ "id": 4711, "name": "Web Frontend", "description": "Web Frontend", "created":"2017-08-16T17:29:14+00:00", "ip": "2001:db8::/64", "type": "ipv6", "server": null, "dns_ptr": [], "blocked": true, "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central" }, "protection": { "delete": true }, "labels": { "key": "value", "key2": "value2" } }`) var s schema.FloatingIP if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } floatingIP := FloatingIPFromSchema(s) if floatingIP.ID != 4711 { t.Errorf("unexpected ID: %v", floatingIP.ID) } if !floatingIP.Blocked { t.Errorf("unexpected value for Blocked: %v", floatingIP.Blocked) } if floatingIP.Name != "Web Frontend" { t.Errorf("unexpected name: %v", floatingIP.Name) } if floatingIP.Description != "Web Frontend" { t.Errorf("unexpected description: %v", floatingIP.Description) } if floatingIP.IP.String() != "2001:db8::" { t.Errorf("unexpected IP: %v", floatingIP.IP) } if floatingIP.Type != FloatingIPTypeIPv6 { t.Errorf("unexpected Type: %v", floatingIP.Type) } if floatingIP.Server != nil { t.Errorf("unexpected Server: %v", floatingIP.Server) } if floatingIP.DNSPtr == nil || floatingIP.DNSPtrForIP(floatingIP.IP) != "" { t.Errorf("unexpected DNS ptr: %v", floatingIP.DNSPtr) } if floatingIP.HomeLocation == nil || floatingIP.HomeLocation.ID != 1 { t.Errorf("unexpected home location: %v", floatingIP.HomeLocation) } if !floatingIP.Protection.Delete { t.Errorf("unexpected Protection.Delete: %v", floatingIP.Protection.Delete) } if floatingIP.Labels["key"] != "value" || floatingIP.Labels["key2"] != "value2" { t.Errorf("unexpected Labels: %v", floatingIP.Labels) } if !floatingIP.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", floatingIP.Created) } }) t.Run("IPv4", func(t *testing.T) { data := []byte(`{ "id": 4711, "description": "Web Frontend", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{ "ip": "131.232.99.1", "dns_ptr": "fip01.example.com" }], "blocked": false, "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 } }`) var s schema.FloatingIP if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } floatingIP := FloatingIPFromSchema(s) if floatingIP.ID != 4711 { t.Errorf("unexpected ID: %v", floatingIP.ID) } if floatingIP.Blocked { t.Errorf("unexpected value for Blocked: %v", floatingIP.Blocked) } if floatingIP.Description != "Web Frontend" { t.Errorf("unexpected description: %v", floatingIP.Description) } if floatingIP.IP.String() != "131.232.99.1" { t.Errorf("unexpected IP: %v", floatingIP.IP) } if floatingIP.Type != FloatingIPTypeIPv4 { t.Errorf("unexpected type: %v", floatingIP.Type) } if floatingIP.Server == nil || floatingIP.Server.ID != 42 { t.Errorf("unexpected server: %v", floatingIP.Server) } if floatingIP.DNSPtr == nil || floatingIP.DNSPtrForIP(floatingIP.IP) != "fip01.example.com" { t.Errorf("unexpected DNS ptr: %v", floatingIP.DNSPtr) } if floatingIP.HomeLocation == nil || floatingIP.HomeLocation.ID != 1 { t.Errorf("unexpected home location: %v", floatingIP.HomeLocation) } }) } func TestISOFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00" }`) var s schema.ISO if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } iso := ISOFromSchema(s) if iso.ID != 4711 { t.Errorf("unexpected ID: %v", iso.ID) } if iso.Name != "FreeBSD-11.0-RELEASE-amd64-dvd1" { t.Errorf("unexpected name: %v", iso.Name) } if iso.Description != "FreeBSD 11.0 x64" { t.Errorf("unexpected description: %v", iso.Description) } if iso.Type != ISOTypePublic { t.Errorf("unexpected type: %v", iso.Type) } if iso.Deprecated.IsZero() { t.Errorf("unexpected value for deprecated: %v", iso.Deprecated) } } func TestDatacenterFromSchema(t *testing.T) { data := []byte(`{ "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, 1, 2, 3 ], "available": [ 1, 1, 2, 3 ] } }`) var s schema.Datacenter if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } datacenter := DatacenterFromSchema(s) if datacenter.ID != 1 { t.Errorf("unexpected ID: %v", datacenter.ID) } if datacenter.Name != "fsn1-dc8" { t.Errorf("unexpected Name: %v", datacenter.Name) } if datacenter.Location == nil || datacenter.Location.ID != 1 { t.Errorf("unexpected Location: %v", datacenter.Location) } if len(datacenter.ServerTypes.Available) != 4 { t.Errorf("unexpected ServerTypes.Available (should be 4): %v", len(datacenter.ServerTypes.Available)) } if len(datacenter.ServerTypes.Supported) != 4 { t.Errorf("unexpected ServerTypes.Supported length (should be 4): %v", len(datacenter.ServerTypes.Supported)) } } func TestLocationFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central" }`) var s schema.Location if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } location := LocationFromSchema(s) if location.ID != 1 { t.Errorf("unexpected ID: %v", location.ID) } if location.Name != "fsn1" { t.Errorf("unexpected Name: %v", location.Name) } if location.Description != "Falkenstein DC Park 1" { t.Errorf("unexpected Description: %v", location.Description) } if location.Country != "DE" { t.Errorf("unexpected Country: %v", location.Country) } if location.City != "Falkenstein" { t.Errorf("unexpected City: %v", location.City) } if location.Latitude != 50.47612 { t.Errorf("unexpected Latitude: %v", location.Latitude) } if location.Longitude != 12.370071 { t.Errorf("unexpected Longitude: %v", location.Longitude) } if location.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", location.NetworkZone) } } func TestServerFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "server.example.com", "status": "running", "created": "2017-08-16T17:29:14+00:00", "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": false, "dns_ptr": "server01.example.com" }, "ipv6": { "ip": "2a01:4f8:1c11:3400::/64", "blocked": false, "dns_ptr": [ { "ip": "2a01:4f8:1c11:3400::1/64", "dns_ptr": "server01.example.com" } ] } }, "private_net": [ { "network": 4711, "ip": "10.0.1.1", "aliases": [ "10.0.1.2" ] } ], "server_type": { "id": 2 }, "outgoing_traffic": 123456, "ingoing_traffic": 7891011, "included_traffic": 654321, "backup_window": "22-02", "rescue_enabled": true, "primary_disk_size": 20, "image": { "id": 4711, "type": "system", "status": "available", "name": "ubuntu16.04-standard-x64", "description": "Ubuntu 16.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2017-08-16T17:29:14+00:00", "created_from": { "id": 1, "name": "Server" }, "bound_to": 1, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": false }, "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public" }, "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" } }, "protection": { "delete": true, "rebuild": true }, "locked": true, "labels": { "key": "value", "key2": "value2" }, "volumes": [123, 456, 789], "placement_group": { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": { "key": "value" }, "name": "my Placement Group", "servers": [ 4711, 4712 ], "type": "spread" } }`) var s schema.Server if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } server := ServerFromSchema(s) if server.ID != 1 { t.Errorf("unexpected ID: %v", server.ID) } if server.Name != "server.example.com" { t.Errorf("unexpected name: %v", server.Name) } if server.Status != ServerStatusRunning { t.Errorf("unexpected status: %v", server.Status) } if !server.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", server.Created) } if server.PublicNet.IPv4.IP.String() != "1.2.3.4" { t.Errorf("unexpected public net IPv4 IP: %v", server.PublicNet.IPv4.IP) } if server.ServerType.ID != 2 { t.Errorf("unexpected server type ID: %v", server.ServerType.ID) } if server.IncludedTraffic != 654321 { t.Errorf("unexpected included traffic: %v", server.IncludedTraffic) } if server.OutgoingTraffic != 123456 { t.Errorf("unexpected outgoing traffic: %v", server.OutgoingTraffic) } if server.IngoingTraffic != 7891011 { t.Errorf("unexpected ingoing traffic: %v", server.IngoingTraffic) } if server.BackupWindow != "22-02" { t.Errorf("unexpected backup window: %v", server.BackupWindow) } if server.PrimaryDiskSize != 20 { t.Errorf("unexpected primary disk size: %v", server.PrimaryDiskSize) } if !server.RescueEnabled { t.Errorf("unexpected rescue enabled state: %v", server.RescueEnabled) } if server.Image == nil || server.Image.ID != 4711 { t.Errorf("unexpected Image: %v", server.Image) } if server.ISO == nil || server.ISO.ID != 4711 { t.Errorf("unexpected ISO: %v", server.ISO) } if server.Datacenter == nil || server.Datacenter.ID != 1 { t.Errorf("unexpected Datacenter: %v", server.Datacenter) } if !server.Locked { t.Errorf("unexpected value for Locked: %v", server.Locked) } if !server.Protection.Delete { t.Errorf("unexpected value for Protection.Delete: %v", server.Protection.Delete) } if !server.Protection.Rebuild { t.Errorf("unexpected value for Protection.Rebuild: %v", server.Protection.Rebuild) } if server.Labels["key"] != "value" || server.Labels["key2"] != "value2" { t.Errorf("unexpected Labels: %v", server.Labels) } if len(s.Volumes) != 3 { t.Errorf("unexpected number of volumes: %v", len(s.Volumes)) } if s.Volumes[0] != 123 || s.Volumes[1] != 456 || s.Volumes[2] != 789 { t.Errorf("unexpected volumes: %v", s.Volumes) } if len(server.PrivateNet) != 1 { t.Errorf("unexpected length of PrivateNet: %v", len(server.PrivateNet)) } if server.PrivateNet[0].Network.ID != 4711 { t.Errorf("unexpected first private net: %v", server.PrivateNet[0]) } if server.PlacementGroup.ID != 897 { t.Errorf("unexpected placement group: %d", server.PlacementGroup.ID) } } func TestServerFromSchemaNoTraffic(t *testing.T) { data := []byte(`{ "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": false, "dns_ptr": "server01.example.com" }, "ipv6": { "ip": "2a01:4f8:1c11:3400::/64", "blocked": false, "dns_ptr": [ { "ip": "2a01:4f8:1c11:3400::1/64", "dns_ptr": "server01.example.com" } ] } }, "outgoing_traffic": null, "ingoing_traffic": null }`) var s schema.Server if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } server := ServerFromSchema(s) if server.OutgoingTraffic != 0 { t.Errorf("unexpected outgoing traffic: %v", server.OutgoingTraffic) } if server.IngoingTraffic != 0 { t.Errorf("unexpected ingoing traffic: %v", server.IngoingTraffic) } } func TestServerPublicNetFromSchema(t *testing.T) { data := []byte(`{ "ipv4": { "ip": "1.2.3.4", "blocked": false, "dns_ptr": "server.example.com" }, "ipv6": { "ip": "2a01:4f8:1c19:1403::/64", "blocked": false, "dns_ptr": [] }, "floating_ips": [4], "firewalls": [ { "id": 23, "status": "applied" } ] }`) var s schema.ServerPublicNet if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } publicNet := ServerPublicNetFromSchema(s) if publicNet.IPv4.IP.String() != "1.2.3.4" { t.Errorf("unexpected IPv4 IP: %v", publicNet.IPv4.IP) } if publicNet.IPv6.Network.String() != "2a01:4f8:1c19:1403::/64" { t.Errorf("unexpected IPv6 IP: %v", publicNet.IPv6.IP) } if len(publicNet.FloatingIPs) != 1 || publicNet.FloatingIPs[0].ID != 4 { t.Errorf("unexpected Floating IPs: %v", publicNet.FloatingIPs) } if len(publicNet.Firewalls) != 1 || publicNet.Firewalls[0].Firewall.ID != 23 || publicNet.Firewalls[0].Status != FirewallStatusApplied { t.Errorf("unexpected Firewalls: %v", publicNet.Firewalls) } } func TestServerPublicNetIPv4FromSchema(t *testing.T) { data := []byte(`{ "ip": "1.2.3.4", "blocked": true, "dns_ptr": "server.example.com" }`) var s schema.ServerPublicNetIPv4 if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } ipv4 := ServerPublicNetIPv4FromSchema(s) if ipv4.IP.String() != "1.2.3.4" { t.Errorf("unexpected IP: %v", ipv4.IP) } if !ipv4.Blocked { t.Errorf("unexpected blocked state: %v", ipv4.Blocked) } if ipv4.DNSPtr != "server.example.com" { t.Errorf("unexpected DNS ptr: %v", ipv4.DNSPtr) } } func TestServerPublicNetIPv6FromSchema(t *testing.T) { data := []byte(`{ "ip": "2a01:4f8:1c11:3400::/64", "blocked": true, "dns_ptr": [ { "ip": "2a01:4f8:1c11:3400::1/64", "blocked": "server01.example.com" } ] }`) var s schema.ServerPublicNetIPv6 if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } ipv6 := ServerPublicNetIPv6FromSchema(s) if ipv6.Network.String() != "2a01:4f8:1c11:3400::/64" { t.Errorf("unexpected IP: %v", ipv6.IP) } if !ipv6.Blocked { t.Errorf("unexpected blocked state: %v", ipv6.Blocked) } if len(ipv6.DNSPtr) != 1 { t.Errorf("unexpected DNS ptr: %v", ipv6.DNSPtr) } } func TestServerPrivateNetFromSchema(t *testing.T) { data := []byte(`{ "network": 4711, "ip": "10.0.1.1", "alias_ips": [ "10.0.1.2" ], "mac_address": "86:00:ff:2a:7d:e1" }`) var s schema.ServerPrivateNet if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } privateNet := ServerPrivateNetFromSchema(s) if privateNet.Network.ID != 4711 { t.Errorf("unexpected Network: %v", privateNet.Network) } if privateNet.IP.String() != "10.0.1.1" { t.Errorf("unexpected IP: %v", privateNet.IP) } if len(privateNet.Aliases) != 1 { t.Errorf("unexpected number of alias IPs: %v", len(privateNet.Aliases)) } if privateNet.Aliases[0].String() != "10.0.1.2" { t.Errorf("unexpected alias IP: %v", privateNet.Aliases[0]) } if privateNet.MACAddress != "86:00:ff:2a:7d:e1" { t.Errorf("unexpected mac address: %v", privateNet.MACAddress) } } func TestServerTypeFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "cx10", "description": "description", "cores": 4, "memory": 1.0, "disk": 20, "storage_type": "local", "cpu_type": "shared", "prices": [ { "location": "fsn1", "price_hourly": { "net": "1", "gross": "1.19" }, "price_monthly": { "net": "1", "gross": "1.19" } } ] }`) var s schema.ServerType if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } serverType := ServerTypeFromSchema(s) if serverType.ID != 1 { t.Errorf("unexpected ID: %v", serverType.ID) } if serverType.Name != "cx10" { t.Errorf("unexpected name: %q", serverType.Name) } if serverType.Description != "description" { t.Errorf("unexpected description: %q", serverType.Description) } if serverType.Cores != 4 { t.Errorf("unexpected cores: %v", serverType.Cores) } if serverType.Memory != 1.0 { t.Errorf("unexpected memory: %v", serverType.Memory) } if serverType.Disk != 20 { t.Errorf("unexpected disk: %v", serverType.Disk) } if serverType.StorageType != StorageTypeLocal { t.Errorf("unexpected storage type: %q", serverType.StorageType) } if serverType.CPUType != CPUTypeShared { t.Errorf("unexpected cpu type: %q", serverType.CPUType) } if len(serverType.Pricings) != 1 { t.Errorf("unexpected number of pricings: %d", len(serverType.Pricings)) } else { if serverType.Pricings[0].Location.Name != "fsn1" { t.Errorf("unexpected location name: %v", serverType.Pricings[0].Location.Name) } if serverType.Pricings[0].Hourly.Net != "1" { t.Errorf("unexpected hourly net price: %v", serverType.Pricings[0].Hourly.Net) } if serverType.Pricings[0].Hourly.Gross != "1.19" { t.Errorf("unexpected hourly gross price: %v", serverType.Pricings[0].Hourly.Gross) } if serverType.Pricings[0].Monthly.Net != "1" { t.Errorf("unexpected monthly net price: %v", serverType.Pricings[0].Monthly.Net) } if serverType.Pricings[0].Monthly.Gross != "1.19" { t.Errorf("unexpected monthly gross price: %v", serverType.Pricings[0].Monthly.Gross) } } } func TestSSHKeyFromSchema(t *testing.T) { data := []byte(`{ "id": 2323, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": { "key": "value", "key2": "value2" }, "created":"2017-08-16T17:29:14+00:00" }`) var s schema.SSHKey if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } sshKey := SSHKeyFromSchema(s) if sshKey.ID != 2323 { t.Errorf("unexpected ID: %v", sshKey.ID) } if sshKey.Name != "My key" { t.Errorf("unexpected name: %v", sshKey.Name) } if sshKey.Fingerprint != "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c" { t.Errorf("unexpected fingerprint: %v", sshKey.Fingerprint) } if sshKey.PublicKey != "ssh-rsa AAAjjk76kgf...Xt" { t.Errorf("unexpected public key: %v", sshKey.PublicKey) } if sshKey.Labels["key"] != "value" || sshKey.Labels["key2"] != "value2" { t.Errorf("unexpected labels: %v", sshKey.Labels) } if !sshKey.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", sshKey.Created) } } func TestErrorFromSchema(t *testing.T) { t.Run("service_error", func(t *testing.T) { data := []byte(`{ "code": "service_error", "message": "An error occurred", "details": {} }`) var s schema.Error if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } err := ErrorFromSchema(s) if err.Code != "service_error" { t.Errorf("unexpected code: %v", err.Code) } if err.Message != "An error occurred" { t.Errorf("unexpected message: %v", err.Message) } }) t.Run("invalid_input", func(t *testing.T) { data := []byte(`{ "code": "invalid_input", "message": "invalid input", "details": { "fields": [ { "name": "broken_field", "messages": ["is required"] } ] } }`) var s schema.Error if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } err := ErrorFromSchema(s) if err.Code != "invalid_input" { t.Errorf("unexpected Code: %v", err.Code) } if err.Message != "invalid input" { t.Errorf("unexpected Message: %v", err.Message) } if d, ok := err.Details.(ErrorDetailsInvalidInput); !ok { t.Fatalf("unexpected Details type (should be ErrorDetailsInvalidInput): %v", err.Details) } else { if len(d.Fields) != 1 { t.Fatalf("unexpected Details.Fields length (should be 1): %v", d.Fields) } if d.Fields[0].Name != "broken_field" { t.Errorf("unexpected Details.Fields[0].Name: %v", d.Fields[0].Name) } if len(d.Fields[0].Messages) != 1 { t.Fatalf("unexpected Details.Fields[0].Messages length (should be 1): %v", d.Fields[0].Messages) } if d.Fields[0].Messages[0] != "is required" { t.Errorf("unexpected Details.Fields[0].Messages[0]: %v", d.Fields[0].Messages[0]) } } }) } func TestPaginationFromSchema(t *testing.T) { data := []byte(`{ "page": 2, "per_page": 25, "previous_page": 1, "next_page": 3, "last_page": 13, "total_entries": 322 }`) var s schema.MetaPagination if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } p := PaginationFromSchema(s) if p.Page != 2 { t.Errorf("unexpected page: %v", p.Page) } if p.PerPage != 25 { t.Errorf("unexpected per page: %v", p.PerPage) } if p.PreviousPage != 1 { t.Errorf("unexpected previous page: %v", p.PreviousPage) } if p.NextPage != 3 { t.Errorf("unexpected next page: %d", p.NextPage) } if p.LastPage != 13 { t.Errorf("unexpected last page: %d", p.LastPage) } if p.TotalEntries != 322 { t.Errorf("unexpected total entries: %d", p.TotalEntries) } } func TestImageFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "type": "system", "status": "available", "name": "ubuntu16.04-standard-x64", "description": "Ubuntu 16.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:55:01Z", "created_from": { "id": 1, "name": "my-server1" }, "bound_to": 1, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": false, "protection": { "delete": true }, "deprecated": "2018-02-28T00:00:00+00:00", "deleted": "2016-01-30T23:55:01+00:00", "labels": { "key": "value", "key2": "value2" } }`) var s schema.Image if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } image := ImageFromSchema(s) if image.ID != 4711 { t.Errorf("unexpected ID: %v", image.ID) } if image.Type != ImageTypeSystem { t.Errorf("unexpected Type: %v", image.Type) } if image.Status != ImageStatusAvailable { t.Errorf("unexpected Status: %v", image.Status) } if image.Name != "ubuntu16.04-standard-x64" { t.Errorf("unexpected Name: %v", image.Name) } if image.Description != "Ubuntu 16.04 Standard 64 bit" { t.Errorf("unexpected Description: %v", image.Description) } if image.ImageSize != 2.3 { t.Errorf("unexpected ImageSize: %v", image.ImageSize) } if image.DiskSize != 10 { t.Errorf("unexpected DiskSize: %v", image.DiskSize) } if !image.Created.Equal(time.Date(2016, 1, 30, 23, 55, 1, 0, time.UTC)) { t.Errorf("unexpected Created: %v", image.Created) } if !image.Deleted.Equal(time.Date(2016, 1, 30, 23, 55, 1, 0, time.UTC)) { t.Errorf("unexpected Deleted: %v", image.Deleted) } if image.CreatedFrom == nil || image.CreatedFrom.ID != 1 || image.CreatedFrom.Name != "my-server1" { t.Errorf("unexpected CreatedFrom: %v", image.CreatedFrom) } if image.BoundTo == nil || image.BoundTo.ID != 1 { t.Errorf("unexpected BoundTo: %v", image.BoundTo) } if image.OSVersion != "16.04" { t.Errorf("unexpected OSVersion: %v", image.OSVersion) } if image.OSFlavor != "ubuntu" { t.Errorf("unexpected OSFlavor: %v", image.OSFlavor) } if image.RapidDeploy { t.Errorf("unexpected RapidDeploy: %v", image.RapidDeploy) } if !image.Protection.Delete { t.Errorf("unexpected Protection.Delete: %v", image.Protection.Delete) } if image.Deprecated.IsZero() { t.Errorf("unexpected value for Deprecated: %v", image.Deprecated) } if image.Labels["key"] != "value" || image.Labels["key2"] != "value2" { t.Errorf("unexpected Labels: %v", image.Labels) } } func TestVolumeFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "created": "2016-01-30T23:50:11+00:00", "name": "db-storage", "status": "creating", "server": 2, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": { "key": "value", "key2": "value2" } }`) var s schema.Volume if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } volume := VolumeFromSchema(s) if volume.ID != 4711 { t.Errorf("unexpected ID: %v", volume.ID) } if volume.Name != "db-storage" { t.Errorf("unexpected name: %v", volume.Name) } if volume.Status != VolumeStatusCreating { t.Errorf("unexpected status: %v", volume.Status) } if !volume.Created.Equal(time.Date(2016, 1, 30, 23, 50, 11, 0, time.UTC)) { t.Errorf("unexpected created date: %s", volume.Created) } if volume.Server == nil { t.Error("no server") } if volume.Server != nil && volume.Server.ID != 2 { t.Errorf("unexpected server ID: %v", volume.Server.ID) } if volume.Location == nil || volume.Location.ID != 1 { t.Errorf("unexpected location: %v", volume.Location) } if volume.Size != 42 { t.Errorf("unexpected size: %v", volume.Size) } if !volume.Protection.Delete { t.Errorf("unexpected value for delete protection: %v", volume.Protection.Delete) } if len(volume.Labels) != 2 { t.Errorf("unexpected number of labels: %d", len(volume.Labels)) } if volume.Labels["key"] != "value" || volume.Labels["key2"] != "value2" { t.Errorf("unexpected labels: %v", volume.Labels) } } func TestNetworkFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "name": "mynet", "created": "2017-08-16T17:29:14+00:00", "ip_range": "10.0.0.0/16", "subnets": [ { "type": "server", "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": [ 4711 ], "protection": { "delete": false }, "labels": {} }`) var s schema.Network if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } network := NetworkFromSchema(s) if network.ID != 4711 { t.Errorf("unexpected ID: %v", network.ID) } if network.Name != "mynet" { t.Errorf("unexpected Name: %v", network.Name) } if !network.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", network.Created) } if network.IPRange.String() != "10.0.0.0/16" { t.Errorf("unexpected IPRange: %v", network.IPRange) } if len(network.Subnets) != 1 { t.Errorf("unexpected length of Subnets: %v", len(network.Subnets)) } if len(network.Routes) != 1 { t.Errorf("unexpected length of Routes: %v", len(network.Routes)) } if len(network.Servers) != 1 { t.Errorf("unexpected length of Servers: %v", len(network.Servers)) } if network.Servers[0].ID != 4711 { t.Errorf("unexpected Server ID: %v", network.Servers[0].ID) } if network.Protection.Delete { t.Errorf("unexpected value for delete protection: %v", network.Protection.Delete) } } func TestNetworkSubnetFromSchema(t *testing.T) { t.Run("type server", func(t *testing.T) { data := []byte(`{ "type": "server", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1" }`) var s schema.NetworkSubnet if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } networkSubnet := NetworkSubnetFromSchema(s) if networkSubnet.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", networkSubnet.NetworkZone) } if networkSubnet.Type != "server" { t.Errorf("unexpected Type: %v", networkSubnet.Type) } if networkSubnet.IPRange.String() != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", networkSubnet.IPRange) } if networkSubnet.Gateway.String() != "10.0.0.1" { t.Errorf("unexpected Gateway: %v", networkSubnet.Gateway) } if networkSubnet.VSwitchID != 0 { t.Errorf("unexpected VSwitchID: %v", networkSubnet.VSwitchID) } }) t.Run("type vswitch", func(t *testing.T) { data := []byte(`{ "type": "vswitch", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1", "vswitch_id": 123 }`) var s schema.NetworkSubnet if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } networkSubnet := NetworkSubnetFromSchema(s) if networkSubnet.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", networkSubnet.NetworkZone) } if networkSubnet.Type != "vswitch" { t.Errorf("unexpected Type: %v", networkSubnet.Type) } if networkSubnet.IPRange.String() != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", networkSubnet.IPRange) } if networkSubnet.Gateway.String() != "10.0.0.1" { t.Errorf("unexpected Gateway: %v", networkSubnet.Gateway) } if networkSubnet.VSwitchID != 123 { t.Errorf("unexpected VSwitchID: %v", networkSubnet.VSwitchID) } }) } func TestNetworkRouteFromSchema(t *testing.T) { data := []byte(`{ "destination": "10.100.1.0/24", "gateway": "10.0.1.1" }`) var s schema.NetworkRoute if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } networkRoute := NetworkRouteFromSchema(s) if networkRoute.Destination.String() != "10.100.1.0/24" { t.Errorf("unexpected Destination: %v", networkRoute.Destination) } if networkRoute.Gateway.String() != "10.0.1.1" { t.Errorf("unexpected Gateway: %v", networkRoute.Gateway) } } func TestLoadBalancerTypeFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "lx11", "description": "LX11", "max_connections": 20000, "max_services": 3, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn1", "price_hourly": { "net": "1", "gross": "1.19" }, "price_monthly": { "net": "1", "gross": "1.19" } } ] }`) var s schema.LoadBalancerType if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } loadBalancerType := LoadBalancerTypeFromSchema(s) if loadBalancerType.ID != 1 { t.Errorf("unexpected ID: %v", loadBalancerType.ID) } if loadBalancerType.Name != "lx11" { t.Errorf("unexpected Name: %v", loadBalancerType.Name) } if loadBalancerType.Description != "LX11" { t.Errorf("unexpected Description: %v", loadBalancerType.Description) } if loadBalancerType.MaxConnections != 20000 { t.Errorf("unexpected MaxConnections: %v", loadBalancerType.MaxConnections) } if loadBalancerType.MaxServices != 3 { t.Errorf("unexpected MaxServices: %v", loadBalancerType.MaxServices) } if loadBalancerType.MaxTargets != 25 { t.Errorf("unexpected MaxTargets: %v", loadBalancerType.MaxTargets) } if loadBalancerType.MaxAssignedCertificates != 10 { t.Errorf("unexpected MaxAssignedCertificates: %v", loadBalancerType.MaxAssignedCertificates) } if len(loadBalancerType.Pricings) != 1 { t.Errorf("unexpected number of pricings: %d", len(loadBalancerType.Pricings)) } else { if loadBalancerType.Pricings[0].Location.Name != "fsn1" { t.Errorf("unexpected location name: %v", loadBalancerType.Pricings[0].Location.Name) } if loadBalancerType.Pricings[0].Hourly.Net != "1" { t.Errorf("unexpected hourly net price: %v", loadBalancerType.Pricings[0].Hourly.Net) } if loadBalancerType.Pricings[0].Hourly.Gross != "1.19" { t.Errorf("unexpected hourly gross price: %v", loadBalancerType.Pricings[0].Hourly.Gross) } if loadBalancerType.Pricings[0].Monthly.Net != "1" { t.Errorf("unexpected monthly net price: %v", loadBalancerType.Pricings[0].Monthly.Net) } if loadBalancerType.Pricings[0].Monthly.Gross != "1.19" { t.Errorf("unexpected monthly gross price: %v", loadBalancerType.Pricings[0].Monthly.Gross) } } } func TestLoadBalancerFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "name": "Web Frontend", "public_net": { "ipv4": { "ip": "131.232.99.1", "dns_ptr": "example.org" }, "ipv6": { "ip": "2001:db8::1", "dns_ptr": "example.com" } }, "private_net": [ { "network": 4711, "ip": "10.0.255.1" } ], "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central" }, "load_balancer_type": { "id": 1, "name": "lx11", "description": "LX11", "max_connections": 20000, "services": 3, "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1", "gross": "1.19" }, "price_monthly": { "net": "1", "gross": "1.19" } } ] }, "outgoing_traffic": 123456, "ingoing_traffic": 7891011, "included_traffic": 654321, "protection": { "delete": false }, "labels": {}, "created": "2016-01-30T23:50:00+00:00", "services": [ { "protocol": "http", "listen_port": 443, "destination_port": 80, "proxyprotocol": false, "sticky_sessions": false, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [ 897 ] }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/" } } } ], "targets": [ { "type": "server", "server": { "id": 80 }, "label_selector": null, "health_status": [ { "listen_port": 443, "status": "healthy" } ], "use_private_ip": false }, { "type": "label_selector", "label_selector": { "selector": "lbt" }, "targets": [ { "type": "server", "server": { "id": 80 }, "health_status": [ { "listen_port": 443, "status": "healthy" } ], "use_private_ip": false } ] } ], "algorithm": { "type": "round_robin" } }`) var s schema.LoadBalancer if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } loadBalancer := LoadBalancerFromSchema(s) if loadBalancer.ID != 4711 { t.Errorf("unexpected ID: %v", loadBalancer.ID) } if loadBalancer.Name != "Web Frontend" { t.Errorf("unexpected Name: %v", loadBalancer.Name) } if loadBalancer.PublicNet.IPv4.IP.String() != "131.232.99.1" { t.Errorf("unexpected IPv4: %v", loadBalancer.PublicNet.IPv4.IP) } if loadBalancer.PublicNet.IPv4.DNSPtr != "example.org" { t.Errorf("unexpected IPv4.DNSPtr: %v", loadBalancer.PublicNet.IPv4.DNSPtr) } if loadBalancer.PublicNet.IPv6.IP.String() != "2001:db8::1" { t.Errorf("unexpected IPv6: %v", loadBalancer.PublicNet.IPv6) } if loadBalancer.PublicNet.IPv6.DNSPtr != "example.com" { t.Errorf("unexpected IPv6.DNSPtr: %v", loadBalancer.PublicNet.IPv6.DNSPtr) } if len(loadBalancer.PrivateNet) != 1 { t.Errorf("unexpected length of PrivateNet: %v", len(loadBalancer.PrivateNet)) } else { if loadBalancer.PrivateNet[0].Network.ID != 4711 { t.Errorf("unexpected Network ID: %v", loadBalancer.PrivateNet[0].Network.ID) } if loadBalancer.PrivateNet[0].IP.String() != "10.0.255.1" { t.Errorf("unexpected Network IP: %v", loadBalancer.PrivateNet[0].IP) } } if loadBalancer.Location == nil || loadBalancer.Location.ID != 1 { t.Errorf("unexpected Location: %v", loadBalancer.Location) } if loadBalancer.LoadBalancerType == nil || loadBalancer.LoadBalancerType.ID != 1 { t.Errorf("unexpected LoadBalancerType: %v", loadBalancer.LoadBalancerType) } if loadBalancer.Protection.Delete { t.Errorf("unexpected value for delete protection: %v", loadBalancer.Protection.Delete) } if !loadBalancer.Created.Equal(time.Date(2016, 01, 30, 23, 50, 00, 0, time.UTC)) { t.Errorf("unexpected created date: %v", loadBalancer.Created) } if len(loadBalancer.Services) != 1 { t.Errorf("unexpected length of Services: %v", len(loadBalancer.Services)) } if len(loadBalancer.Targets) != 2 { t.Errorf("unexpected length of Targets: %v", len(loadBalancer.Targets)) } if loadBalancer.Algorithm.Type != "round_robin" { t.Errorf("unexpected Algorithm.Type: %v", loadBalancer.Algorithm.Type) } if loadBalancer.IncludedTraffic != 654321 { t.Errorf("unexpected included traffic: %v", loadBalancer.IncludedTraffic) } if loadBalancer.OutgoingTraffic != 123456 { t.Errorf("unexpected outgoing traffic: %v", loadBalancer.OutgoingTraffic) } if loadBalancer.IngoingTraffic != 7891011 { t.Errorf("unexpected ingoing traffic: %v", loadBalancer.IngoingTraffic) } } func TestLoadBalancerServiceFromSchema(t *testing.T) { data := []byte(`{ "protocol": "http", "listen_port": 443, "destination_port": 80, "proxyprotocol": false, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [ 897 ], "redirect_http": true, "sticky_sessions": true }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": "", "status_codes":["200","201"], "tls": false } } }`) var s schema.LoadBalancerService if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } loadBalancerService := LoadBalancerServiceFromSchema(s) if loadBalancerService.Protocol != "http" { t.Errorf("unexpected Protocol: %v", loadBalancerService.Protocol) } if loadBalancerService.ListenPort != 443 { t.Errorf("unexpected ListenPort: %v", loadBalancerService.ListenPort) } if loadBalancerService.DestinationPort != 80 { t.Errorf("unexpected DestinationPort: %v", loadBalancerService.DestinationPort) } if loadBalancerService.Proxyprotocol { t.Errorf("unexpected ProxyProtocol: %v", loadBalancerService.Proxyprotocol) } if loadBalancerService.HTTP.CookieName != "HCLBSTICKY" { t.Errorf("unexpected HTTP.CookieName: %v", loadBalancerService.HTTP.CookieName) } if loadBalancerService.HTTP.CookieLifetime.Seconds() != 300 { t.Errorf("unexpected HTTP.CookieLifetime: %v", loadBalancerService.HTTP.CookieLifetime.Seconds()) } if loadBalancerService.HTTP.Certificates[0].ID != 897 { t.Errorf("unexpected Certificates[0].ID : %v", loadBalancerService.HTTP.Certificates[0].ID) } if !loadBalancerService.HTTP.RedirectHTTP { t.Errorf("unexpected HTTP.RedirectHTTP: %v", loadBalancerService.HTTP.RedirectHTTP) } if !loadBalancerService.HTTP.StickySessions { t.Errorf("unexpected HTTP.StickySessions: %v", loadBalancerService.HTTP.StickySessions) } if loadBalancerService.HealthCheck.Protocol != "http" { t.Errorf("unexpected HealthCheck.Protocol: %v", loadBalancerService.HealthCheck.Protocol) } if loadBalancerService.HealthCheck.Port != 4711 { t.Errorf("unexpected HealthCheck.Port: %v", loadBalancerService.HealthCheck.Port) } if loadBalancerService.HealthCheck.Interval.Seconds() != 15 { t.Errorf("unexpected HealthCheck.Interval: %v", loadBalancerService.HealthCheck.Interval) } if loadBalancerService.HealthCheck.Timeout.Seconds() != 10 { t.Errorf("unexpected HealthCheck.Timeout: %v", loadBalancerService.HealthCheck.Timeout) } if loadBalancerService.HealthCheck.Retries != 3 { t.Errorf("unexpected HealthCheck.Retries: %v", loadBalancerService.HealthCheck.Retries) } if loadBalancerService.HealthCheck.HTTP.Domain != "example.com" { t.Errorf("unexpected HealthCheck.HTTP.Domain: %v", loadBalancerService.HealthCheck.HTTP.Domain) } if loadBalancerService.HealthCheck.HTTP.Path != "/" { t.Errorf("unexpected HealthCheck.HTTP.Path: %v", loadBalancerService.HealthCheck.HTTP.Path) } if loadBalancerService.HealthCheck.HTTP.Response != "" { t.Errorf("unexpected HealthCheck.HTTP.Response: %v", loadBalancerService.HealthCheck.HTTP.Response) } if loadBalancerService.HealthCheck.HTTP.TLS { t.Errorf("unexpected HealthCheck.HTTP.TLS: %v", loadBalancerService.HealthCheck.HTTP.TLS) } if len(loadBalancerService.HealthCheck.HTTP.StatusCodes) != 2 { t.Errorf("unexpected len(HealthCheck.HTTP.StatusCodes): %v", len(loadBalancerService.HealthCheck.HTTP.StatusCodes)) } else { if loadBalancerService.HealthCheck.HTTP.StatusCodes[0] != "200" { t.Errorf("unexpected HealthCheck.HTTP.StatusCodes[0]: %v", loadBalancerService.HealthCheck.HTTP.StatusCodes[0]) } if loadBalancerService.HealthCheck.HTTP.StatusCodes[1] != "201" { t.Errorf("unexpected HealthCheck.HTTP.StatusCodes[1]: %v", loadBalancerService.HealthCheck.HTTP.StatusCodes[1]) } } } func TestLoadBalancerTargetFromSchema(t *testing.T) { t.Run("server target", func(t *testing.T) { data := []byte(`{ "type": "server", "server": { "id": 80 }, "label_selector": null, "health_status": [ { "listen_port": 443, "status": "healthy" } ], "use_private_ip": false }`) var s schema.LoadBalancerTarget if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } loadBalancerTarget := LoadBalancerTargetFromSchema(s) if loadBalancerTarget.Type != "server" { t.Errorf("unexpected Type: %v", loadBalancerTarget.Type) } if loadBalancerTarget.Server == nil || loadBalancerTarget.Server.Server.ID != 80 { t.Errorf("unexpected Server: %v", loadBalancerTarget.Server) } if loadBalancerTarget.LabelSelector != nil { t.Errorf("unexpected LabelSelector.Selector: %v", loadBalancerTarget.LabelSelector) } if loadBalancerTarget.UsePrivateIP { t.Errorf("unexpected UsePrivateIP: %v", loadBalancerTarget.UsePrivateIP) } if len(loadBalancerTarget.HealthStatus) != 1 { t.Errorf("unexpected Health Status length: %v", len(loadBalancerTarget.HealthStatus)) } else { if loadBalancerTarget.HealthStatus[0].ListenPort != 443 { t.Errorf("unexpected HealthStatus[0].ListenPort: %v", loadBalancerTarget.HealthStatus[0].ListenPort) } if loadBalancerTarget.HealthStatus[0].Status != LoadBalancerTargetHealthStatusStatusHealthy { t.Errorf("unexpected HealthStatus[0].Status: %v", loadBalancerTarget.HealthStatus[0].Status) } } }) t.Run("label_selector target", func(t *testing.T) { data := []byte(`{ "type": "label_selector", "label_selector": { "selector": "lbt" }, "targets": [ { "type": "server", "server": { "id": 80 }, "health_status": [ { "listen_port": 443, "status": "healthy" } ] } ] }`) var s schema.LoadBalancerTarget if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } loadBalancerTarget := LoadBalancerTargetFromSchema(s) if loadBalancerTarget.Type != "label_selector" { t.Errorf("unexpected Type: %v", loadBalancerTarget.Type) } if loadBalancerTarget.LabelSelector == nil || loadBalancerTarget.LabelSelector.Selector != "lbt" { t.Errorf("unexpected LabelSelector: %v", loadBalancerTarget.LabelSelector) } if loadBalancerTarget.Server != nil { t.Errorf("unexpected LabelSelector.Server: %v", loadBalancerTarget.Server) } if len(loadBalancerTarget.Targets) != 1 { t.Errorf("unexpected Targets length: %v", len(loadBalancerTarget.Targets)) } else { if loadBalancerTarget.Targets[0].Server == nil || loadBalancerTarget.Targets[0].Server.Server.ID != 80 { t.Errorf("unexpected loadBalancerTarget.Targets[0].Server.Server.ID: %v", loadBalancerTarget.Targets[0].Server.Server.ID) } if len(loadBalancerTarget.Targets[0].HealthStatus) != 1 { t.Errorf("unexpected Targets length: %v", len(loadBalancerTarget.Targets[0].HealthStatus)) } else { if loadBalancerTarget.Targets[0].HealthStatus[0].ListenPort != 443 { t.Errorf("unexpected HealthStatus[0].ListenPort: %v", loadBalancerTarget.Targets[0].HealthStatus[0].ListenPort) } if loadBalancerTarget.Targets[0].HealthStatus[0].Status != LoadBalancerTargetHealthStatusStatusHealthy { t.Errorf("unexpected HealthStatus[0].Status: %v", loadBalancerTarget.Targets[0].HealthStatus[0].Status) } } } }) t.Run("ip target", func(t *testing.T) { var s schema.LoadBalancerTarget data := []byte(`{ "type": "ip", "ip": { "ip": "1.2.3.4" } }`) if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } lbTgt := LoadBalancerTargetFromSchema(s) if lbTgt.Type != LoadBalancerTargetTypeIP { t.Errorf("unexpected Type: %s", lbTgt.Type) } if lbTgt.IP.IP != "1.2.3.4" { t.Errorf("unexpected IP: %s", lbTgt.IP.IP) } }) } func TestCertificateFromSchema(t *testing.T) { tests := []struct { name string data string expected Certificate }{ { name: "uploaded certificate", data: `{ "id": 897, "name": "my website cert", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2016-01-30T23:50:00+00:00", "not_valid_before": "2016-01-30T23:51:00+00:00", "not_valid_after": "2016-01-30T23:55:00+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com" ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "used_by": [ {"id": 42, "type": "loadbalancer"} ] }`, expected: Certificate{ ID: 897, Name: "my website cert", Type: "uploaded", Certificate: "-----BEGIN CERTIFICATE-----\n...", Created: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:50:00+00:00"), NotValidBefore: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:51:00+00:00"), NotValidAfter: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:55:00+00:00"), DomainNames: []string{"example.com", "webmail.example.com", "www.example.com"}, Fingerprint: "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", UsedBy: []CertificateUsedByRef{ {ID: 42, Type: "loadbalancer"}, }, }, }, { name: "managed certificate", data: `{ "id": 898, "name": "managed certificate", "labels": {}, "type": "managed", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2016-01-30T23:50:00+00:00", "not_valid_before": "2016-01-30T23:51:00+00:00", "not_valid_after": "2016-01-30T23:55:00+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com" ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": { "issuance": "completed", "renewal": "failed", "error": { "code": "dns_zone_not_found", "message": "DNS zone not found" } }, "used_by": [ {"id": 42, "type": "loadbalancer"} ] }`, expected: Certificate{ ID: 898, Name: "managed certificate", Type: "managed", Certificate: "-----BEGIN CERTIFICATE-----\n...", Created: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:50:00+00:00"), NotValidBefore: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:51:00+00:00"), NotValidAfter: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:55:00+00:00"), DomainNames: []string{"example.com", "webmail.example.com", "www.example.com"}, Fingerprint: "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", Status: &CertificateStatus{ Issuance: CertificateStatusTypeCompleted, Renewal: CertificateStatusTypeFailed, Error: &Error{ Code: "dns_zone_not_found", Message: "DNS zone not found", }, }, UsedBy: []CertificateUsedByRef{ {ID: 42, Type: "loadbalancer"}, }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { var s schema.Certificate err := json.Unmarshal([]byte(tt.data), &s) assert.NoError(t, err) actual := CertificateFromSchema(s) assert.Equal(t, &tt.expected, actual) }) } } func TestPricingFromSchema(t *testing.T) { data := []byte(`{ "currency": "EUR", "vat_rate": "19.00", "image": { "price_per_gb_month": { "net": "1", "gross": "1.19" } }, "floating_ip": { "price_monthly": { "net": "1", "gross": "1.19" } }, "floating_ips": [ { "prices": [ { "location": "fsn1", "price_monthly": { "gross": "1.19", "net": "1" } } ], "type": "ipv4" } ], "traffic": { "price_per_tb": { "net": "1", "gross": "1.19" } }, "server_backup": { "percentage": "20" }, "server_types": [ { "id": 4, "name": "CX11", "prices": [ { "location": "fsn1", "price_hourly": { "net": "1", "gross": "1.19" }, "price_monthly": { "net": "1", "gross": "1.19" } } ] } ], "load_balancer_types": [ { "id": 4, "name": "LX11", "prices": [ { "location": "fsn1", "price_hourly": { "net": "1", "gross": "1.19" }, "price_monthly": { "net": "1", "gross": "1.19" } } ] } ], "volume": { "price_per_gb_month": { "net": "1", "gross": "1.19" } } }`) var s schema.Pricing if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } pricing := PricingFromSchema(s) if pricing.Image.PerGBMonth.Currency != "EUR" { t.Errorf("unexpected Image.PerGBMonth.Currency: %v", pricing.Image.PerGBMonth.Currency) } if pricing.Image.PerGBMonth.VATRate != "19.00" { t.Errorf("unexpected Image.PerGBMonth.VATRate: %v", pricing.Image.PerGBMonth.VATRate) } if pricing.Image.PerGBMonth.Net != "1" { t.Errorf("unexpected Image.PerGBMonth.Net: %v", pricing.Image.PerGBMonth.Net) } if pricing.Image.PerGBMonth.Gross != "1.19" { t.Errorf("unexpected Image.PerGBMonth.Gross: %v", pricing.Image.PerGBMonth.Gross) } if pricing.FloatingIP.Monthly.Currency != "EUR" { t.Errorf("unexpected FloatingIP.Monthly.Currency: %v", pricing.FloatingIP.Monthly.Currency) } if pricing.FloatingIP.Monthly.VATRate != "19.00" { t.Errorf("unexpected FloatingIP.Monthly.VATRate: %v", pricing.FloatingIP.Monthly.VATRate) } if pricing.FloatingIP.Monthly.Net != "1" { t.Errorf("unexpected FloatingIP.Monthly.Net: %v", pricing.FloatingIP.Monthly.Net) } if pricing.FloatingIP.Monthly.Gross != "1.19" { t.Errorf("unexpected FloatingIP.Monthly.Gross: %v", pricing.FloatingIP.Monthly.Gross) } if len(pricing.FloatingIPs) != 1 { t.Errorf("unexpected number of Floating IPs: %d", len(pricing.FloatingIPs)) } else { p := pricing.FloatingIPs[0] if p.Type != FloatingIPTypeIPv4 { t.Errorf("unexpected .Type: %s", p.Type) } if len(p.Pricings) != 1 { t.Errorf("unexpected number of prices: %d", len(p.Pricings)) } else { if p.Pricings[0].Location.Name != "fsn1" { t.Errorf("unexpected Location.Name: %v", p.Pricings[0].Location.Name) } if p.Pricings[0].Monthly.Currency != "EUR" { t.Errorf("unexpected Monthly.Currency: %v", p.Pricings[0].Monthly.Currency) } if p.Pricings[0].Monthly.VATRate != "19.00" { t.Errorf("unexpected Monthly.VATRate: %v", p.Pricings[0].Monthly.VATRate) } if p.Pricings[0].Monthly.Net != "1" { t.Errorf("unexpected Monthly.Net: %v", p.Pricings[0].Monthly.Net) } if p.Pricings[0].Monthly.Gross != "1.19" { t.Errorf("unexpected Monthly.Gross: %v", p.Pricings[0].Monthly.Gross) } } } if pricing.Volume.PerGBMonthly.Currency != "EUR" { t.Errorf("unexpected Traffic.PerTB.Currency: %v", pricing.Volume.PerGBMonthly.Currency) } if pricing.Volume.PerGBMonthly.VATRate != "19.00" { t.Errorf("unexpected Traffic.PerTB.VATRate: %v", pricing.Volume.PerGBMonthly.VATRate) } if pricing.Volume.PerGBMonthly.Net != "1" { t.Errorf("unexpected Traffic.PerTB.Net: %v", pricing.Volume.PerGBMonthly.Net) } if pricing.Volume.PerGBMonthly.Gross != "1.19" { t.Errorf("unexpected Traffic.PerTB.Gross: %v", pricing.Volume.PerGBMonthly.Gross) } if pricing.Traffic.PerTB.Currency != "EUR" { t.Errorf("unexpected Traffic.PerTB.Currency: %v", pricing.Traffic.PerTB.Currency) } if pricing.Traffic.PerTB.VATRate != "19.00" { t.Errorf("unexpected Traffic.PerTB.VATRate: %v", pricing.Traffic.PerTB.VATRate) } if pricing.Traffic.PerTB.Net != "1" { t.Errorf("unexpected Traffic.PerTB.Net: %v", pricing.Traffic.PerTB.Net) } if pricing.Traffic.PerTB.Gross != "1.19" { t.Errorf("unexpected Traffic.PerTB.Gross: %v", pricing.Traffic.PerTB.Gross) } if pricing.ServerBackup.Percentage != "20" { t.Errorf("unexpected ServerBackup.Percentage: %v", pricing.ServerBackup.Percentage) } if len(pricing.ServerTypes) != 1 { t.Errorf("unexpected number of server types: %d", len(pricing.ServerTypes)) } else { p := pricing.ServerTypes[0] if p.ServerType.ID != 4 { t.Errorf("unexpected ServerType.ID: %d", p.ServerType.ID) } if p.ServerType.Name != "CX11" { t.Errorf("unexpected ServerType.Name: %v", p.ServerType.Name) } if len(p.Pricings) != 1 { t.Errorf("unexpected number of prices: %d", len(p.Pricings)) } else { if p.Pricings[0].Location.Name != "fsn1" { t.Errorf("unexpected Location.Name: %v", p.Pricings[0].Location.Name) } if p.Pricings[0].Hourly.Currency != "EUR" { t.Errorf("unexpected Hourly.Currency: %v", p.Pricings[0].Hourly.Currency) } if p.Pricings[0].Hourly.VATRate != "19.00" { t.Errorf("unexpected Hourly.VATRate: %v", p.Pricings[0].Hourly.VATRate) } if p.Pricings[0].Hourly.Net != "1" { t.Errorf("unexpected Hourly.Net: %v", p.Pricings[0].Hourly.Net) } if p.Pricings[0].Hourly.Gross != "1.19" { t.Errorf("unexpected Hourly.Gross: %v", p.Pricings[0].Hourly.Gross) } if p.Pricings[0].Monthly.Currency != "EUR" { t.Errorf("unexpected Monthly.Currency: %v", p.Pricings[0].Monthly.Currency) } if p.Pricings[0].Monthly.VATRate != "19.00" { t.Errorf("unexpected Monthly.VATRate: %v", p.Pricings[0].Monthly.VATRate) } if p.Pricings[0].Monthly.Net != "1" { t.Errorf("unexpected Monthly.Net: %v", p.Pricings[0].Monthly.Net) } if p.Pricings[0].Monthly.Gross != "1.19" { t.Errorf("unexpected Monthly.Gross: %v", p.Pricings[0].Monthly.Gross) } } } if len(pricing.LoadBalancerTypes) != 1 { t.Errorf("unexpected number of Load Balancer types: %d", len(pricing.LoadBalancerTypes)) } else { p := pricing.LoadBalancerTypes[0] if p.LoadBalancerType.ID != 4 { t.Errorf("unexpected LoadBalancerType.ID: %d", p.LoadBalancerType.ID) } if p.LoadBalancerType.Name != "LX11" { t.Errorf("unexpected LoadBalancerType.Name: %v", p.LoadBalancerType.Name) } if len(p.Pricings) != 1 { t.Errorf("unexpected number of prices: %d", len(p.Pricings)) } else { if p.Pricings[0].Location.Name != "fsn1" { t.Errorf("unexpected Location.Name: %v", p.Pricings[0].Location.Name) } if p.Pricings[0].Hourly.Currency != "EUR" { t.Errorf("unexpected Hourly.Currency: %v", p.Pricings[0].Hourly.Currency) } if p.Pricings[0].Hourly.VATRate != "19.00" { t.Errorf("unexpected Hourly.VATRate: %v", p.Pricings[0].Hourly.VATRate) } if p.Pricings[0].Hourly.Net != "1" { t.Errorf("unexpected Hourly.Net: %v", p.Pricings[0].Hourly.Net) } if p.Pricings[0].Hourly.Gross != "1.19" { t.Errorf("unexpected Hourly.Gross: %v", p.Pricings[0].Hourly.Gross) } if p.Pricings[0].Monthly.Currency != "EUR" { t.Errorf("unexpected Monthly.Currency: %v", p.Pricings[0].Monthly.Currency) } if p.Pricings[0].Monthly.VATRate != "19.00" { t.Errorf("unexpected Monthly.VATRate: %v", p.Pricings[0].Monthly.VATRate) } if p.Pricings[0].Monthly.Net != "1" { t.Errorf("unexpected Monthly.Net: %v", p.Pricings[0].Monthly.Net) } if p.Pricings[0].Monthly.Gross != "1.19" { t.Errorf("unexpected Monthly.Gross: %v", p.Pricings[0].Monthly.Gross) } } } } func TestLoadBalancerCreateOptsToSchema(t *testing.T) { testCases := map[string]struct { Opts LoadBalancerCreateOpts Request schema.LoadBalancerCreateRequest }{ "minimal": { Opts: LoadBalancerCreateOpts{ Name: "test", LoadBalancerType: &LoadBalancerType{Name: "lb11"}, Algorithm: &LoadBalancerAlgorithm{Type: LoadBalancerAlgorithmTypeRoundRobin}, NetworkZone: NetworkZoneEUCentral, }, Request: schema.LoadBalancerCreateRequest{ Name: "test", LoadBalancerType: "lb11", Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{ Type: string(LoadBalancerAlgorithmTypeRoundRobin), }, NetworkZone: String(string(NetworkZoneEUCentral)), }, }, "all set": { Opts: LoadBalancerCreateOpts{ Name: "test", LoadBalancerType: &LoadBalancerType{Name: "lb11"}, Algorithm: &LoadBalancerAlgorithm{Type: LoadBalancerAlgorithmTypeRoundRobin}, NetworkZone: NetworkZoneEUCentral, Labels: map[string]string{"foo": "bar"}, PublicInterface: Bool(true), Network: &Network{ID: 3}, Services: []LoadBalancerCreateOptsService{ { Protocol: LoadBalancerServiceProtocolHTTP, DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &LoadBalancerCreateOptsServiceHTTP{ CookieName: String("keks"), CookieLifetime: Duration(5 * time.Minute), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: []*Certificate{{ID: 1}, {ID: 2}}, }, HealthCheck: &LoadBalancerCreateOptsServiceHealthCheck{ Protocol: LoadBalancerServiceProtocolHTTP, Port: Int(80), Interval: Duration(5 * time.Second), Timeout: Duration(1 * time.Second), Retries: Int(3), HTTP: &LoadBalancerCreateOptsServiceHealthCheckHTTP{ Domain: String("example.com"), Path: String("/health"), Response: String("ok"), StatusCodes: []string{"2??", "3??"}, TLS: Bool(true), }, }, }, }, Targets: []LoadBalancerCreateOptsTarget{ { Type: LoadBalancerTargetTypeServer, Server: LoadBalancerCreateOptsTargetServer{ Server: &Server{ID: 5}, }, }, { Type: LoadBalancerTargetTypeIP, IP: LoadBalancerCreateOptsTargetIP{IP: "1.2.3.4"}, }, }, }, Request: schema.LoadBalancerCreateRequest{ Name: "test", LoadBalancerType: "lb11", Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{ Type: string(LoadBalancerAlgorithmTypeRoundRobin), }, NetworkZone: String(string(NetworkZoneEUCentral)), Labels: func() *map[string]string { labels := map[string]string{"foo": "bar"} return &labels }(), PublicInterface: Bool(true), Network: Int(3), Services: []schema.LoadBalancerCreateRequestService{ { Protocol: string(LoadBalancerServiceProtocolHTTP), DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &schema.LoadBalancerCreateRequestServiceHTTP{ CookieName: String("keks"), CookieLifetime: Int(5 * 60), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: intSlice([]int{1, 2}), }, HealthCheck: &schema.LoadBalancerCreateRequestServiceHealthCheck{ Protocol: string(LoadBalancerServiceProtocolHTTP), Port: Int(80), Interval: Int(5), Timeout: Int(1), Retries: Int(3), HTTP: &schema.LoadBalancerCreateRequestServiceHealthCheckHTTP{ Domain: String("example.com"), Path: String("/health"), Response: String("ok"), StatusCodes: stringSlice([]string{"2??", "3??"}), TLS: Bool(true), }, }, }, }, Targets: []schema.LoadBalancerCreateRequestTarget{ { Type: "server", Server: &schema.LoadBalancerCreateRequestTargetServer{ ID: 5, }, }, { Type: "ip", IP: &schema.LoadBalancerCreateRequestTargetIP{ IP: "1.2.3.4", }, }, }, }, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { req := loadBalancerCreateOptsToSchema(testCase.Opts) if !cmp.Equal(testCase.Request, req) { t.Log(cmp.Diff(testCase.Request, req)) t.Fail() } }) } } func TestLoadBalancerAddServiceOptsToSchema(t *testing.T) { testCases := map[string]struct { Opts LoadBalancerAddServiceOpts Request schema.LoadBalancerActionAddServiceRequest }{ "minimal": { Opts: LoadBalancerAddServiceOpts{ Protocol: LoadBalancerServiceProtocolHTTP, }, Request: schema.LoadBalancerActionAddServiceRequest{ Protocol: string(LoadBalancerServiceProtocolHTTP), }, }, "all set": { Opts: LoadBalancerAddServiceOpts{ Protocol: LoadBalancerServiceProtocolHTTP, DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &LoadBalancerAddServiceOptsHTTP{ CookieName: String("keks"), CookieLifetime: Duration(5 * time.Minute), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: []*Certificate{{ID: 1}, {ID: 2}}, }, HealthCheck: &LoadBalancerAddServiceOptsHealthCheck{ Protocol: LoadBalancerServiceProtocolHTTP, Port: Int(80), Interval: Duration(5 * time.Second), Timeout: Duration(1 * time.Second), Retries: Int(3), HTTP: &LoadBalancerAddServiceOptsHealthCheckHTTP{ Domain: String("example.com"), Path: String("/health"), Response: String("ok"), StatusCodes: []string{"2??", "3??"}, TLS: Bool(true), }, }, }, Request: schema.LoadBalancerActionAddServiceRequest{ Protocol: string(LoadBalancerServiceProtocolHTTP), DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &schema.LoadBalancerActionAddServiceRequestHTTP{ CookieName: String("keks"), CookieLifetime: Int(5 * 60), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: intSlice([]int{1, 2}), }, HealthCheck: &schema.LoadBalancerActionAddServiceRequestHealthCheck{ Protocol: string(LoadBalancerServiceProtocolHTTP), Port: Int(80), Interval: Int(5), Timeout: Int(1), Retries: Int(3), HTTP: &schema.LoadBalancerActionAddServiceRequestHealthCheckHTTP{ Domain: String("example.com"), Path: String("/health"), Response: String("ok"), StatusCodes: stringSlice([]string{"2??", "3??"}), TLS: Bool(true), }, }, }, }, "no health check": { Opts: LoadBalancerAddServiceOpts{ Protocol: LoadBalancerServiceProtocolHTTP, DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &LoadBalancerAddServiceOptsHTTP{ CookieName: String("keks"), CookieLifetime: Duration(5 * time.Minute), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: []*Certificate{{ID: 1}, {ID: 2}}, }, }, Request: schema.LoadBalancerActionAddServiceRequest{ Protocol: string(LoadBalancerServiceProtocolHTTP), DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &schema.LoadBalancerActionAddServiceRequestHTTP{ CookieName: String("keks"), CookieLifetime: Int(5 * 60), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: intSlice([]int{1, 2}), }, HealthCheck: nil, }, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { req := loadBalancerAddServiceOptsToSchema(testCase.Opts) if !cmp.Equal(testCase.Request, req) { t.Log(cmp.Diff(testCase.Request, req)) t.Fail() } }) } } func TestLoadBalancerUpdateServiceOptsToSchema(t *testing.T) { testCases := map[string]struct { Opts LoadBalancerUpdateServiceOpts Request schema.LoadBalancerActionUpdateServiceRequest }{ "empty": { Opts: LoadBalancerUpdateServiceOpts{}, Request: schema.LoadBalancerActionUpdateServiceRequest{}, }, "all set": { Opts: LoadBalancerUpdateServiceOpts{ Protocol: LoadBalancerServiceProtocolHTTP, DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &LoadBalancerUpdateServiceOptsHTTP{ CookieName: String("keks"), CookieLifetime: Duration(5 * time.Minute), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: []*Certificate{{ID: 1}, {ID: 2}}, }, HealthCheck: &LoadBalancerUpdateServiceOptsHealthCheck{ Protocol: LoadBalancerServiceProtocolHTTP, Port: Int(80), Interval: Duration(5 * time.Second), Timeout: Duration(1 * time.Second), Retries: Int(3), HTTP: &LoadBalancerUpdateServiceOptsHealthCheckHTTP{ Domain: String("example.com"), Path: String("/health"), Response: String("ok"), StatusCodes: []string{"2??", "3??"}, TLS: Bool(true), }, }, }, Request: schema.LoadBalancerActionUpdateServiceRequest{ Protocol: String(string(LoadBalancerServiceProtocolHTTP)), DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &schema.LoadBalancerActionUpdateServiceRequestHTTP{ CookieName: String("keks"), CookieLifetime: Int(5 * 60), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: intSlice([]int{1, 2}), }, HealthCheck: &schema.LoadBalancerActionUpdateServiceRequestHealthCheck{ Protocol: String(string(LoadBalancerServiceProtocolHTTP)), Port: Int(80), Interval: Int(5), Timeout: Int(1), Retries: Int(3), HTTP: &schema.LoadBalancerActionUpdateServiceRequestHealthCheckHTTP{ Domain: String("example.com"), Path: String("/health"), Response: String("ok"), StatusCodes: stringSlice([]string{"2??", "3??"}), TLS: Bool(true), }, }, }, }, "no health check": { Opts: LoadBalancerUpdateServiceOpts{ Protocol: LoadBalancerServiceProtocolHTTP, DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &LoadBalancerUpdateServiceOptsHTTP{ CookieName: String("keks"), CookieLifetime: Duration(5 * time.Minute), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: []*Certificate{{ID: 1}, {ID: 2}}, }, }, Request: schema.LoadBalancerActionUpdateServiceRequest{ Protocol: String(string(LoadBalancerServiceProtocolHTTP)), DestinationPort: Int(80), Proxyprotocol: Bool(true), HTTP: &schema.LoadBalancerActionUpdateServiceRequestHTTP{ CookieName: String("keks"), CookieLifetime: Int(5 * 60), RedirectHTTP: Bool(true), StickySessions: Bool(true), Certificates: intSlice([]int{1, 2}), }, HealthCheck: nil, }, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { req := loadBalancerUpdateServiceOptsToSchema(testCase.Opts) if !cmp.Equal(testCase.Request, req) { t.Log(cmp.Diff(testCase.Request, req)) t.Fail() } }) } } func TestServerMetricsFromSchema(t *testing.T) { tests := []struct { name string respFn func() *schema.ServerGetMetricsResponse expected *ServerMetrics expectedErr string }{ { name: "values not tuples", respFn: func() *schema.ServerGetMetricsResponse { var resp schema.ServerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{ "cpu": { Values: []interface{}{"some value"}, }, } return &resp }, expectedErr: "failed to convert value to tuple: some value", }, { name: "invalid tuple size", respFn: func() *schema.ServerGetMetricsResponse { var resp schema.ServerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{ "cpu": { Values: []interface{}{ []interface{}{1435781471.622, "43", "something else"}, }, }, } return &resp }, expectedErr: "invalid tuple size: 3: [1.435781471622e+09 43 something else]", }, { name: "invalid time stamp", respFn: func() *schema.ServerGetMetricsResponse { var resp schema.ServerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{ "cpu": { Values: []interface{}{ []interface{}{"1435781471.622", "43"}, }, }, } return &resp }, expectedErr: "convert to float64: 1435781471.622", }, { name: "invalid value", respFn: func() *schema.ServerGetMetricsResponse { var resp schema.ServerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{ "cpu": { Values: []interface{}{ []interface{}{1435781471.622, 43}, }, }, } return &resp }, expectedErr: "not a string: 43", }, { name: "valid response", respFn: func() *schema.ServerGetMetricsResponse { var resp schema.ServerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{ "cpu": { Values: []interface{}{ []interface{}{1435781470.622, "42"}, []interface{}{1435781471.622, "43"}, }, }, "disk.0.iops.read": { Values: []interface{}{ []interface{}{1435781480.622, "100"}, []interface{}{1435781481.622, "150"}, }, }, "disk.0.iops.write": { Values: []interface{}{ []interface{}{1435781480.622, "50"}, []interface{}{1435781481.622, "55"}, }, }, "network.0.pps.in": { Values: []interface{}{ []interface{}{1435781490.622, "70"}, []interface{}{1435781491.622, "75"}, }, }, "network.0.pps.out": { Values: []interface{}{ []interface{}{1435781590.622, "60"}, []interface{}{1435781591.622, "65"}, }, }, } return &resp }, expected: &ServerMetrics{ Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), TimeSeries: map[string][]ServerMetricsValue{ "cpu": { {Timestamp: 1435781470.622, Value: "42"}, {Timestamp: 1435781471.622, Value: "43"}, }, "disk.0.iops.read": { {Timestamp: 1435781480.622, Value: "100"}, {Timestamp: 1435781481.622, Value: "150"}, }, "disk.0.iops.write": { {Timestamp: 1435781480.622, Value: "50"}, {Timestamp: 1435781481.622, Value: "55"}, }, "network.0.pps.in": { {Timestamp: 1435781490.622, Value: "70"}, {Timestamp: 1435781491.622, Value: "75"}, }, "network.0.pps.out": { {Timestamp: 1435781590.622, Value: "60"}, {Timestamp: 1435781591.622, Value: "65"}, }, }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { resp := tt.respFn() actual, err := serverMetricsFromSchema(resp) if err != nil && tt.expectedErr == "" { t.Fatalf("expected no error; got: %v", err) } if err != nil && tt.expectedErr != err.Error() { t.Fatalf("expected error: %s; got: %v", tt.expectedErr, err) } if !cmp.Equal(tt.expected, actual) { t.Errorf("unexpected result:\n%s", cmp.Diff(tt.expected, actual)) } }) } } func TestLoadBalancerMetricsFromSchema(t *testing.T) { tests := []struct { name string respFn func() *schema.LoadBalancerGetMetricsResponse expected *LoadBalancerMetrics expectedErr string }{ { name: "values not tuples", respFn: func() *schema.LoadBalancerGetMetricsResponse { var resp schema.LoadBalancerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{ "open_connections": { Values: []interface{}{"some value"}, }, } return &resp }, expectedErr: "failed to convert value to tuple: some value", }, { name: "invalid tuple size", respFn: func() *schema.LoadBalancerGetMetricsResponse { var resp schema.LoadBalancerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{ "open_connections": { Values: []interface{}{ []interface{}{1435781471.622, "43", "something else"}, }, }, } return &resp }, expectedErr: "invalid tuple size: 3: [1.435781471622e+09 43 something else]", }, { name: "invalid time stamp", respFn: func() *schema.LoadBalancerGetMetricsResponse { var resp schema.LoadBalancerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{ "open_connections": { Values: []interface{}{ []interface{}{"1435781471.622", "43"}, }, }, } return &resp }, expectedErr: "convert to float64: 1435781471.622", }, { name: "invalid value", respFn: func() *schema.LoadBalancerGetMetricsResponse { var resp schema.LoadBalancerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{ "open_connections": { Values: []interface{}{ []interface{}{1435781471.622, 43}, }, }, } return &resp }, expectedErr: "not a string: 43", }, { name: "valid response", respFn: func() *schema.LoadBalancerGetMetricsResponse { var resp schema.LoadBalancerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{ "open_connections": { Values: []interface{}{ []interface{}{1435781470.622, "42"}, []interface{}{1435781471.622, "43"}, }, }, "connections_per_second": { Values: []interface{}{ []interface{}{1435781480.622, "100"}, []interface{}{1435781481.622, "150"}, }, }, "requests_per_second": { Values: []interface{}{ []interface{}{1435781480.622, "50"}, []interface{}{1435781481.622, "55"}, }, }, "bandwidth.in": { Values: []interface{}{ []interface{}{1435781490.622, "70"}, []interface{}{1435781491.622, "75"}, }, }, "bandwidth.out": { Values: []interface{}{ []interface{}{1435781590.622, "60"}, []interface{}{1435781591.622, "65"}, }, }, } return &resp }, expected: &LoadBalancerMetrics{ Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), TimeSeries: map[string][]LoadBalancerMetricsValue{ "open_connections": { {Timestamp: 1435781470.622, Value: "42"}, {Timestamp: 1435781471.622, Value: "43"}, }, "connections_per_second": { {Timestamp: 1435781480.622, Value: "100"}, {Timestamp: 1435781481.622, Value: "150"}, }, "requests_per_second": { {Timestamp: 1435781480.622, Value: "50"}, {Timestamp: 1435781481.622, Value: "55"}, }, "bandwidth.in": { {Timestamp: 1435781490.622, Value: "70"}, {Timestamp: 1435781491.622, Value: "75"}, }, "bandwidth.out": { {Timestamp: 1435781590.622, Value: "60"}, {Timestamp: 1435781591.622, Value: "65"}, }, }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { resp := tt.respFn() actual, err := loadBalancerMetricsFromSchema(resp) if err != nil && tt.expectedErr == "" { t.Fatalf("expected no error; got: %v", err) } if err != nil && tt.expectedErr != err.Error() { t.Fatalf("expected error: %s; got: %v", tt.expectedErr, err) } if !cmp.Equal(tt.expected, actual) { t.Errorf("unexpected result:\n%s", cmp.Diff(tt.expected, actual)) } }) } } func TestFirewallFromSchema(t *testing.T) { data := []byte(`{ "id": 897, "name": "my firewall", "labels": { "key": "value", "key2": "value2" }, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128" ], "destination_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128" ], "protocol": "tcp", "port": "80", "description": "allow http in" } ], "applied_to": [ { "server": { "id": 42 }, "type": "server" }, { "label_selector": { "selector": "a=b" }, "type": "label_selector" } ] } `) var f schema.Firewall if err := json.Unmarshal(data, &f); err != nil { t.Fatal(err) } firewall := FirewallFromSchema(f) if firewall.ID != 897 { t.Errorf("unexpected ID: %v", firewall.ID) } if firewall.Name != "my firewall" { t.Errorf("unexpected Name: %v", firewall.Name) } if firewall.Labels["key"] != "value" || firewall.Labels["key2"] != "value2" { t.Errorf("unexpected Labels: %v", firewall.Labels) } if !firewall.Created.Equal(time.Date(2016, 01, 30, 23, 50, 00, 0, time.UTC)) { t.Errorf("unexpected Created date: %v", firewall.Created) } if len(firewall.Rules) != 1 { t.Errorf("unexpected Rules count: %d", len(firewall.Rules)) } if firewall.Rules[0].Direction != FirewallRuleDirectionIn { t.Errorf("unexpected Rule Direction: %s", firewall.Rules[0].Direction) } if len(firewall.Rules[0].SourceIPs) != 3 { t.Errorf("unexpected Rule SourceIPs count: %d", len(firewall.Rules[0].SourceIPs)) } if len(firewall.Rules[0].DestinationIPs) != 3 { t.Errorf("unexpected Rule DestinationIPs count: %d", len(firewall.Rules[0].DestinationIPs)) } if firewall.Rules[0].Protocol != FirewallRuleProtocolTCP { t.Errorf("unexpected Rule Protocol: %s", firewall.Rules[0].Protocol) } if *firewall.Rules[0].Port != "80" { t.Errorf("unexpected Rule Port: %s", *firewall.Rules[0].Port) } if *firewall.Rules[0].Description != "allow http in" { t.Errorf("unexpected Rule Description: %s", *firewall.Rules[0].Description) } if len(firewall.AppliedTo) != 2 { t.Errorf("unexpected UsedBy count: %d", len(firewall.AppliedTo)) } if firewall.AppliedTo[0].Type != FirewallResourceTypeServer { t.Errorf("unexpected UsedBy Type: %s", firewall.AppliedTo[0].Type) } if firewall.AppliedTo[0].Server.ID != 42 { t.Errorf("unexpected UsedBy Server ID: %d", firewall.AppliedTo[0].Server.ID) } if firewall.AppliedTo[1].Type != FirewallResourceTypeLabelSelector { t.Errorf("unexpected UsedBy Type: %s", firewall.AppliedTo[0].Type) } if firewall.AppliedTo[1].LabelSelector.Selector != "a=b" { t.Errorf("unexpected UsedBy Label Selector: %s", firewall.AppliedTo[1].LabelSelector.Selector) } } func TestPlacementGroupFromSchema(t *testing.T) { data := []byte(`{ "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": { "key": "value" }, "name": "my Placement Group", "servers": [ 4711, 4712 ], "type": "spread" } `) var g schema.PlacementGroup if err := json.Unmarshal(data, &g); err != nil { t.Fatal(err) } placementGroup := PlacementGroupFromSchema(g) if placementGroup.ID != 897 { t.Errorf("unexpected ID %d", placementGroup.ID) } if placementGroup.Name != "my Placement Group" { t.Errorf("unexpected Name %s", placementGroup.Name) } if placementGroup.Labels["key"] != "value" { t.Errorf("unexpected Labels: %v", placementGroup.Labels) } if !placementGroup.Created.Equal(time.Date(2019, 01, 8, 12, 10, 00, 0, time.UTC)) { t.Errorf("unexpected Created date: %v", placementGroup.Created) } if len(placementGroup.Servers) != 2 { t.Errorf("unexpected Servers %v", placementGroup.Servers) } if placementGroup.Type != PlacementGroupTypeSpread { t.Errorf("unexpected Type %s", placementGroup.Type) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/server.go000066400000000000000000001016271414442033200240300ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net" "net/http" "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 PrimaryDiskSize int PlacementGroup *PlacementGroup } // 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" ) // FirewallStatus specifies a Firewall's status. type FirewallStatus string const ( // FirewallStatusPending is the status when a Firewall is pending. FirewallStatusPending FirewallStatus = "pending" // FirewallStatusApplied is the status when a Firewall is applied. FirewallStatusApplied FirewallStatus = "applied" ) // ServerPublicNet represents a server's public network. type ServerPublicNet struct { IPv4 ServerPublicNetIPv4 IPv6 ServerPublicNetIPv6 FloatingIPs []*FloatingIP Firewalls []*ServerFirewallStatus } // 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()] } // ServerFirewallStatus represents a Firewall and its status on a Server's // network interface. type ServerFirewallStatus struct { Firewall Firewall Status FirewallStatus } // ServerRescueType represents rescue types. type ServerRescueType string // List of rescue types. const ( ServerRescueTypeLinux32 ServerRescueType = "linux32" ServerRescueTypeLinux64 ServerRescueType = "linux64" ServerRescueTypeFreeBSD64 ServerRescueType = "freebsd64" ) // changeDNSPtr changes or resets the reverse DNS pointer for a IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (s *Server) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) { reqBody := schema.ServerActionChangeDNSPtrRequest{ IP: ip.String(), DNSPtr: ptr, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", s.ID) req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionChangeDNSPtrResponse{} resp, err := client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // GetDNSPtrForIP searches for the dns assigned to the given IP address. // It returns an error if there is no dns set for the given IP address. func (s *Server) GetDNSPtrForIP(ip net.IP) (string, error) { if net.IP.Equal(s.PublicNet.IPv4.IP, ip) { return s.PublicNet.IPv4.DNSPtr, nil } else if dns, ok := s.PublicNet.IPv6.DNSPtr[ip.String()]; ok { return dns, nil } return "", DNSNotFoundError{ip} } // 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 Firewalls []*ServerCreateFirewall PlacementGroup *PlacementGroup } // ServerCreateFirewall defines which Firewalls to apply when creating a Server. type ServerCreateFirewall struct { Firewall Firewall } // 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) } for _, firewall := range opts.Firewalls { reqBody.Firewalls = append(reqBody.Firewalls, schema.ServerCreateFirewalls{ Firewall: firewall.Firewall.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 } } if opts.PlacementGroup != nil { reqBody.PlacementGroup = opts.PlacementGroup.ID } 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) { netIP := net.ParseIP(ip) if netIP == nil { return nil, nil, InvalidIPError{ip} } return server.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr) } // 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 } // ServerMetricType is the type of available metrics for servers. type ServerMetricType string // Available types of server metrics. See Hetzner Cloud API documentation for // details. const ( ServerMetricCPU ServerMetricType = "cpu" ServerMetricDisk ServerMetricType = "disk" ServerMetricNetwork ServerMetricType = "network" ) // ServerGetMetricsOpts configures the call to get metrics for a Server. type ServerGetMetricsOpts struct { Types []ServerMetricType Start time.Time End time.Time Step int } func (o *ServerGetMetricsOpts) addQueryParams(req *http.Request) error { query := req.URL.Query() if len(o.Types) == 0 { return fmt.Errorf("no metric types specified") } for _, typ := range o.Types { query.Add("type", string(typ)) } if o.Start.IsZero() { return fmt.Errorf("no start time specified") } query.Add("start", o.Start.Format(time.RFC3339)) if o.End.IsZero() { return fmt.Errorf("no end time specified") } query.Add("end", o.End.Format(time.RFC3339)) if o.Step > 0 { query.Add("step", strconv.Itoa(o.Step)) } req.URL.RawQuery = query.Encode() return nil } // ServerMetrics contains the metrics requested for a Server. type ServerMetrics struct { Start time.Time End time.Time Step float64 TimeSeries map[string][]ServerMetricsValue } // ServerMetricsValue represents a single value in a time series of metrics. type ServerMetricsValue struct { Timestamp float64 Value string } // GetMetrics obtains metrics for Server. func (c *ServerClient) GetMetrics(ctx context.Context, server *Server, opts ServerGetMetricsOpts) (*ServerMetrics, *Response, error) { var respBody schema.ServerGetMetricsResponse if server == nil { return nil, nil, fmt.Errorf("illegal argument: server is nil") } path := fmt.Sprintf("/servers/%d/metrics", server.ID) req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, fmt.Errorf("new request: %v", err) } if err := opts.addQueryParams(req); err != nil { return nil, nil, fmt.Errorf("add query params: %v", err) } resp, err := c.client.Do(req, &respBody) if err != nil { return nil, nil, fmt.Errorf("get metrics: %v", err) } ms, err := serverMetricsFromSchema(&respBody) if err != nil { return nil, nil, fmt.Errorf("convert response body: %v", err) } return ms, resp, nil } func (c *ServerClient) AddToPlacementGroup(ctx context.Context, server *Server, placementGroup *PlacementGroup) (*Action, *Response, error) { reqBody := schema.ServerActionAddToPlacementGroupRequest{ PlacementGroup: placementGroup.ID, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/add_to_placement_group", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionAddToPlacementGroupResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } func (c *ServerClient) RemoveFromPlacementGroup(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/remove_from_placement_group", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionRemoveFromPlacementGroupResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/server_test.go000066400000000000000000001515301414442033200250650ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net" "net/http" "net/url" "strconv" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestServerClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerGetResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } t.Run("called via Get", func(t *testing.T) { server, _, err := env.Client.Server.Get(ctx, "1") if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } }) } func TestServerClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if server != nil { t.Fatal("expected no server") } } func TestServerClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myserver" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerListResponse{ Servers: []schema.Server{ { ID: 1, Name: "myserver", }, }, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByName(ctx, "myserver") if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } t.Run("via Get", func(t *testing.T) { server, _, err := env.Client.Server.Get(ctx, "myserver") if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } }) } func TestServerClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myserver" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerListResponse{ Servers: []schema.Server{}, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByName(ctx, "myserver") if err != nil { t.Fatal(err) } if server != nil { t.Fatal("unexpected server") } } func TestServerClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() server, _, err := env.Client.Server.GetByName(ctx, "") if err != nil { t.Fatal(err) } if server != nil { t.Fatal("unexpected server") } } func TestServersList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ServerListResponse{ Servers: []schema.Server{ {ID: 1}, {ID: 2}, }, }) }) opts := ServerListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() servers, _, err := env.Client.Server.List(ctx, opts) if err != nil { t.Fatal(err) } if len(servers) != 2 { t.Fatal("expected 2 servers") } } func TestServersAll(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Servers []schema.Server `json:"servers"` Meta schema.Meta `json:"meta"` }{ Servers: []schema.Server{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() servers, err := env.Client.Server.All(ctx) if err != nil { t.Fatalf("Servers.List failed: %s", err) } if len(servers) != 3 { t.Fatalf("expected 3 servers; got %d", len(servers)) } if servers[0].ID != 1 || servers[1].ID != 2 || servers[2].ID != 3 { t.Errorf("unexpected servers") } } func TestServersAllWithOpts(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } if name := r.URL.Query().Get("name"); name != "my-server" { t.Errorf("unexpected name: %s", name) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Servers []schema.Server `json:"servers"` Meta schema.Meta `json:"meta"` }{ Servers: []schema.Server{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() opts := ServerListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}, Name: "my-server"} servers, err := env.Client.Server.AllWithOpts(ctx, opts) if err != nil { t.Fatalf("Servers.List failed: %s", err) } if len(servers) != 3 { t.Fatalf("expected 3 servers; got %d", len(servers)) } if servers[0].ID != 1 || servers[1].ID != 2 || servers[2].ID != 3 { t.Errorf("unexpected servers") } } func TestServersCreateWithSSHKeys(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.SSHKeys) != 2 || reqBody.SSHKeys[0] != 1 || reqBody.SSHKeys[1] != 2 { t.Errorf("unexpected SSH keys: %v", reqBody.SSHKeys) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, SSHKeys: []*SSHKey{ {ID: 1}, {ID: 2}, }, }) if err != nil { t.Fatalf("Server.Create failed: %s", err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if result.RootPassword != "" { t.Errorf("expected no root password, got: %v", result.RootPassword) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestServersCreateWithoutSSHKeys(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.SSHKeys) != 0 { t.Errorf("expected no SSH keys, but got %v", reqBody.SSHKeys) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, RootPassword: String("test"), }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, }) if err != nil { t.Fatalf("Server.Create failed: %s", err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if result.RootPassword != "test" { t.Errorf("unexpected root password: %v", result.RootPassword) } } func TestServersCreateWithVolumes(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.Volumes) != 2 || reqBody.Volumes[0] != 1 || reqBody.Volumes[1] != 2 { t.Errorf("unexpected Volumes: %v", reqBody.Volumes) } if reqBody.Automount == nil || !*reqBody.Automount { t.Errorf("unexpected Automount: %v", reqBody.Automount) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Volumes: []*Volume{ {ID: 1}, {ID: 2}, }, Automount: Bool(true), }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestServersCreateWithNetworks(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.Networks) != 2 || reqBody.Networks[0] != 1 || reqBody.Networks[1] != 2 { t.Errorf("unexpected Networks: %v", reqBody.Networks) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Networks: []*Network{ {ID: 1}, {ID: 2}, }, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestServersCreateWithDatacenterID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Datacenter != "1" { t.Errorf("unexpected datacenter: %v", reqBody.Datacenter) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Datacenter: &Datacenter{ID: 1}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithDatacenterName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Datacenter != "dc1" { t.Errorf("unexpected datacenter: %v", reqBody.Datacenter) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Datacenter: &Datacenter{Name: "dc1"}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithLocationID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Location != "1" { t.Errorf("unexpected location: %v", reqBody.Location) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Location: &Location{ID: 1}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithLocationName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Location != "loc1" { t.Errorf("unexpected location: %v", reqBody.Location) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Location: &Location{Name: "loc1"}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithUserData(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.UserData != "---user data---" { t.Errorf("unexpected userdata: %v", reqBody.UserData) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, UserData: "---user data---", }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithFirewalls(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.Firewalls) != 2 || reqBody.Firewalls[0].Firewall != 1 || reqBody.Firewalls[1].Firewall != 2 { t.Errorf("unexpected Firewalls: %v", reqBody.Firewalls) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Firewalls: []*ServerCreateFirewall{ {Firewall: Firewall{ID: 1}}, {Firewall: Firewall{ID: 2}}, }, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestServersCreateWithLabels(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.SSHKeys) != 0 { t.Errorf("expected no SSH keys, but got %v", reqBody.SSHKeys) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, RootPassword: String("test"), }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Labels: map[string]string{"key": "value"}, }) if err != nil { t.Fatalf("Server.Create failed: %s", err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } } func TestServersCreateWithoutStarting(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.StartAfterCreate == nil || *reqBody.StartAfterCreate { t.Errorf("unexpected value for start_after_create: %v", reqBody.StartAfterCreate) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, StartAfterCreate: Bool(false), }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServerCreateWithPlacementGroup(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.PlacementGroup != 123 { t.Errorf("unexpected placement group id %d", reqBody.PlacementGroup) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, PlacementGroup: &schema.PlacementGroup{ ID: 123, }, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, PlacementGroup: &PlacementGroup{ID: 123}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %d", result.Server.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } if result.Server.PlacementGroup.ID != 123 { t.Errorf("unexpected placement group ID: %d", result.Server.PlacementGroup.ID) } } func TestServersDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() server = &Server{ID: 1} ) _, err := env.Client.Server.Delete(ctx, server) if err != nil { t.Fatalf("Server.Delete failed: %s", err) } } func TestServerClientUpdate(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.ServerUpdateResponse{ Server: schema.Server{ ID: 1, }, }) }) opts := ServerUpdateOpts{ Name: "test", } updatedServer, _, err := env.Client.Server.Update(ctx, server, opts) if err != nil { t.Fatal(err) } if updatedServer.ID != 1 { t.Errorf("unexpected server ID: %v", updatedServer.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.ServerUpdateResponse{ Server: schema.Server{ ID: 1, }, }) }) opts := ServerUpdateOpts{ Labels: map[string]string{"key": "value"}, } updatedServer, _, err := env.Client.Server.Update(ctx, server, opts) if err != nil { t.Fatal(err) } if updatedServer.ID != 1 { t.Errorf("unexpected server ID: %v", updatedServer.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.ServerUpdateResponse{ Server: schema.Server{ ID: 1, }, }) }) opts := ServerUpdateOpts{} updatedServer, _, err := env.Client.Server.Update(ctx, server, opts) if err != nil { t.Fatal(err) } if updatedServer.ID != 1 { t.Errorf("unexpected server ID: %v", updatedServer.ID) } }) } func TestServerClientPoweron(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/poweron", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionPoweronResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Poweron(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientReboot(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/reboot", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionRebootResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Reboot(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientReset(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/reset", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionResetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Reset(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientShutdown(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/shutdown", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionShutdownResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Shutdown(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientPoweroff(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/poweroff", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionPoweroffResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Poweroff(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientResetPassword(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/reset_password", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionResetPasswordResponse{ Action: schema.Action{ ID: 1, }, RootPassword: "secret", }) }) ctx := context.Background() result, _, err := env.Client.Server.ResetPassword(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.RootPassword != "secret" { t.Errorf("unexpected root password: %v", result.RootPassword) } } func TestServerClientCreateImageNoOptions(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/create_image", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionCreateImageResponse{ Action: schema.Action{ ID: 1, }, Image: schema.Image{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.CreateImage(ctx, &Server{ID: 1}, nil) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.Image.ID != 1 { t.Errorf("unexpected image ID: %d", result.Image.ID) } } func TestServerClientCreateImageWithOptions(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/create_image", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionCreateImageRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type == nil || *reqBody.Type != "backup" { t.Errorf("unexpected type: %v", reqBody.Type) } if reqBody.Description == nil || *reqBody.Description != "my backup" { t.Errorf("unexpected description: %v", reqBody.Description) } json.NewEncoder(w).Encode(schema.ServerActionCreateImageResponse{ Action: schema.Action{ ID: 1, }, Image: schema.Image{ ID: 1, }, }) }) ctx := context.Background() opts := &ServerCreateImageOpts{ Type: ImageTypeBackup, Description: String("my backup"), } result, _, err := env.Client.Server.CreateImage(ctx, &Server{ID: 1}, opts) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.Image.ID != 1 { t.Errorf("unexpected image ID: %d", result.Image.ID) } } func TestServerClientEnableRescue(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/enable_rescue", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionEnableRescueRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type == nil || *reqBody.Type != "linux64" { t.Errorf("unexpected type: %v", reqBody.Type) } if len(reqBody.SSHKeys) != 2 || reqBody.SSHKeys[0] != 1 || reqBody.SSHKeys[1] != 2 { t.Errorf("unexpected SSH keys: %v", reqBody.SSHKeys) } json.NewEncoder(w).Encode(schema.ServerActionEnableRescueResponse{ Action: schema.Action{ ID: 1, }, RootPassword: "test", }) }) ctx := context.Background() opts := ServerEnableRescueOpts{ Type: ServerRescueTypeLinux64, SSHKeys: []*SSHKey{ {ID: 1}, {ID: 2}, }, } result, _, err := env.Client.Server.EnableRescue(ctx, &Server{ID: 1}, opts) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.RootPassword != "test" { t.Errorf("unexpected root password: %s", result.RootPassword) } } func TestServerClientDisableRescue(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/disable_rescue", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionDisableRescueResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.DisableRescue(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientRebuild(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with image ID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/rebuild", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionRebuildRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if id, ok := reqBody.Image.(float64); !ok || id != 1 { t.Errorf("unexpected image ID: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerRebuildOpts{ Image: &Image{ID: 1}, } action, _, err := env.Client.Server.Rebuild(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("with image name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/rebuild", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionRebuildRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if name, ok := reqBody.Image.(string); !ok || name != "debian-9" { t.Errorf("unexpected image name: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerRebuildOpts{ Image: &Image{Name: "debian-9"}, } action, _, err := env.Client.Server.Rebuild(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientAttachISO(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with ISO ID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_iso", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionAttachISORequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if id, ok := reqBody.ISO.(float64); !ok || id != 1 { t.Errorf("unexpected ISO ID: %v", reqBody.ISO) } json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{ Action: schema.Action{ ID: 1, }, }) }) iso := &ISO{ID: 1} action, _, err := env.Client.Server.AttachISO(ctx, server, iso) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("with ISO name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_iso", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionAttachISORequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if name, ok := reqBody.ISO.(string); !ok || name != "debian.iso" { t.Errorf("unexpected ISO name: %v", reqBody.ISO) } json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{ Action: schema.Action{ ID: 1, }, }) }) iso := &ISO{Name: "debian.iso"} action, _, err := env.Client.Server.AttachISO(ctx, server, iso) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientDetachISO(t *testing.T) { env := newTestEnv() defer env.Teardown() var ( ctx = context.Background() server = &Server{ID: 1} ) env.Mux.HandleFunc("/servers/1/actions/detach_iso", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionDetachISOResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.DetachISO(ctx, server) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientEnableBackup(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with a backup window", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/enable_backup", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionEnableBackupRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.BackupWindow == nil || *reqBody.BackupWindow != "9-17" { t.Errorf("unexpected backup window: %v", reqBody.BackupWindow) } json.NewEncoder(w).Encode(schema.ServerActionEnableBackupResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.EnableBackup(ctx, server, "9-17") if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("without a backup window", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/enable_backup", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionEnableBackupRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.BackupWindow != nil { t.Errorf("unexpected backup window: %v", reqBody.BackupWindow) } json.NewEncoder(w).Encode(schema.ServerActionEnableBackupResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.EnableBackup(ctx, server, "") if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientDisableBackup(t *testing.T) { env := newTestEnv() defer env.Teardown() var ( ctx = context.Background() server = &Server{ID: 1} ) env.Mux.HandleFunc("/servers/1/actions/disable_backup", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionDisableBackupResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.DisableBackup(ctx, server) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientChangeType(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with server type ID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_type", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionChangeTypeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if id, ok := reqBody.ServerType.(float64); !ok || id != 1 { t.Errorf("unexpected server type ID: %v", reqBody.ServerType) } if !reqBody.UpgradeDisk { t.Error("expected to upgrade disk") } json.NewEncoder(w).Encode(schema.ServerActionChangeTypeResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerChangeTypeOpts{ ServerType: &ServerType{ID: 1}, UpgradeDisk: true, } action, _, err := env.Client.Server.ChangeType(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("with server type name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_type", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionChangeTypeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if name, ok := reqBody.ServerType.(string); !ok || name != "type" { t.Errorf("unexpected server type name: %v", reqBody.ServerType) } if !reqBody.UpgradeDisk { t.Error("expected to upgrade disk") } json.NewEncoder(w).Encode(schema.ServerActionChangeTypeResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerChangeTypeOpts{ ServerType: &ServerType{Name: "type"}, UpgradeDisk: true, } action, _, err := env.Client.Server.ChangeType(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientChangeProtection(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("enable delete and rebuild protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } if reqBody.Rebuild == nil || *reqBody.Rebuild != true { t.Errorf("unexpected rebuild: %v", reqBody.Rebuild) } json.NewEncoder(w).Encode(schema.ServerActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerChangeProtectionOpts{ Delete: Bool(true), Rebuild: Bool(true), } action, _, err := env.Client.Server.ChangeProtection(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } func TestServerClientRequestConsole(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/request_console", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionRequestConsoleResponse{ Action: schema.Action{ ID: 1, }, WSSURL: "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c", Password: "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x", }) }) ctx := context.Background() result, _, err := env.Client.Server.RequestConsole(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.WSSURL != "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c" { t.Errorf("unexpected WebSocket URL: %v", result.WSSURL) } if result.Password != "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x" { t.Errorf("unexpected password: %v", result.Password) } } func TestServerClientAttachToNetwork(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("attach to network", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_to_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionAttachToNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerAttachToNetworkOpts{ Network: &Network{ID: 1}, } action, _, err := env.Client.Server.AttachToNetwork(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) t.Run("attach to network with additional parameters", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_to_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionAttachToNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } if reqBody.IP == nil || *reqBody.IP != "10.0.1.1" { t.Errorf("unexpected IP: %v", *reqBody.IP) } if len(reqBody.AliasIPs) == 0 || *reqBody.AliasIPs[0] != "10.0.1.1" { t.Errorf("unexpected AliasIPs: %v", *reqBody.IP) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) ip := net.ParseIP("10.0.1.1") aliasIPs := []net.IP{ ip, } opts := ServerAttachToNetworkOpts{ Network: &Network{ID: 1}, IP: ip, AliasIPs: aliasIPs, } action, _, err := env.Client.Server.AttachToNetwork(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } func TestServerClientDetachFromNetwork(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/detach_from_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionDetachFromNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerDetachFromNetworkOpts{ Network: &Network{ID: 1}, } action, _, err := env.Client.Server.DetachFromNetwork(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestServerClientChangeAliasIP(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_alias_ips", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionChangeAliasIPsRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } if len(reqBody.AliasIPs) == 0 || reqBody.AliasIPs[0] != "10.0.1.1" { t.Errorf("unexpected AliasIPs: %v", reqBody.AliasIPs[0]) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) ip := net.ParseIP("10.0.1.1") aliasIPs := []net.IP{ ip, } opts := ServerChangeAliasIPsOpts{ Network: &Network{ID: 1}, AliasIPs: aliasIPs, } action, _, err := env.Client.Server.ChangeAliasIPs(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestServerGetMetrics(t *testing.T) { tests := []struct { name string server *Server opts ServerGetMetricsOpts respStatus int respFn func() schema.ServerGetMetricsResponse expected ServerMetrics expectedErr string }{ { name: "cpu metrics", server: &Server{ID: 1}, opts: ServerGetMetricsOpts{ Types: []ServerMetricType{ServerMetricCPU}, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, respFn: func() schema.ServerGetMetricsResponse { var resp schema.ServerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{ "cpu": { Values: []interface{}{ []interface{}{1435781470.622, "42"}, []interface{}{1435781471.622, "43"}, }, }, } return resp }, expected: ServerMetrics{ Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), TimeSeries: map[string][]ServerMetricsValue{ "cpu": { {Timestamp: 1435781470.622, Value: "42"}, {Timestamp: 1435781471.622, Value: "43"}, }, }, }, }, { name: "all metrics", server: &Server{ID: 2}, opts: ServerGetMetricsOpts{ Types: []ServerMetricType{ ServerMetricCPU, ServerMetricDisk, ServerMetricNetwork, }, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, respFn: func() schema.ServerGetMetricsResponse { var resp schema.ServerGetMetricsResponse resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z") resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z") resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{ "cpu": { Values: []interface{}{ []interface{}{1435781470.622, "42"}, []interface{}{1435781471.622, "43"}, }, }, "disk.0.iops.read": { Values: []interface{}{ []interface{}{1435781480.622, "100"}, []interface{}{1435781481.622, "150"}, }, }, "disk.0.iops.write": { Values: []interface{}{ []interface{}{1435781480.622, "50"}, []interface{}{1435781481.622, "55"}, }, }, "network.0.pps.in": { Values: []interface{}{ []interface{}{1435781490.622, "70"}, []interface{}{1435781491.622, "75"}, }, }, "network.0.pps.out": { Values: []interface{}{ []interface{}{1435781590.622, "60"}, []interface{}{1435781591.622, "65"}, }, }, } return resp }, expected: ServerMetrics{ Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), TimeSeries: map[string][]ServerMetricsValue{ "cpu": { {Timestamp: 1435781470.622, Value: "42"}, {Timestamp: 1435781471.622, Value: "43"}, }, "disk.0.iops.read": { {Timestamp: 1435781480.622, Value: "100"}, {Timestamp: 1435781481.622, Value: "150"}, }, "disk.0.iops.write": { {Timestamp: 1435781480.622, Value: "50"}, {Timestamp: 1435781481.622, Value: "55"}, }, "network.0.pps.in": { {Timestamp: 1435781490.622, Value: "70"}, {Timestamp: 1435781491.622, Value: "75"}, }, "network.0.pps.out": { {Timestamp: 1435781590.622, Value: "60"}, {Timestamp: 1435781591.622, Value: "65"}, }, }, }, }, { name: "missing metrics types", server: &Server{ID: 3}, opts: ServerGetMetricsOpts{ Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, expectedErr: "add query params: no metric types specified", }, { name: "no start time", server: &Server{ID: 4}, opts: ServerGetMetricsOpts{ Types: []ServerMetricType{ServerMetricCPU}, End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, expectedErr: "add query params: no start time specified", }, { name: "no end time", server: &Server{ID: 5}, opts: ServerGetMetricsOpts{ Types: []ServerMetricType{ServerMetricCPU}, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), }, expectedErr: "add query params: no end time specified", }, { name: "call to backend API fails", server: &Server{ID: 6}, opts: ServerGetMetricsOpts{ Types: []ServerMetricType{ServerMetricCPU}, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, respStatus: http.StatusInternalServerError, expectedErr: "get metrics: hcloud: server responded with status code 500", }, { name: "no server passed", opts: ServerGetMetricsOpts{ Types: []ServerMetricType{ServerMetricCPU}, Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"), End: mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"), }, expectedErr: "illegal argument: server is nil", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { env := newTestEnv() defer env.Teardown() if tt.server != nil { path := fmt.Sprintf("/servers/%d/metrics", tt.server.ID) env.Mux.HandleFunc(path, func(rw http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Errorf("expected GET; got %s", r.Method) } opts := serverMetricsOptsFromURL(t, r.URL) if !cmp.Equal(tt.opts, opts) { t.Errorf("unexpected opts: url: %s\n%v", r.URL.String(), cmp.Diff(tt.opts, opts)) } status := tt.respStatus if status == 0 { status = http.StatusOK } rw.WriteHeader(status) if tt.respFn != nil { resp := tt.respFn() if err := json.NewEncoder(rw).Encode(resp); err != nil { t.Errorf("failed to encode response: %v", err) } } }) } ctx := context.Background() actual, _, err := env.Client.Server.GetMetrics(ctx, tt.server, tt.opts) if tt.expectedErr != "" { if tt.expectedErr != err.Error() { t.Errorf("expected err: %v; got: %v", tt.expectedErr, err) } return } if err != nil { t.Fatalf("failed to get server metrics: %v", err) } if !cmp.Equal(&tt.expected, actual) { t.Errorf("Actual metrics did not equal expected: %s", cmp.Diff(&tt.expected, actual)) } }) } } func serverMetricsOptsFromURL(t *testing.T, u *url.URL) ServerGetMetricsOpts { var opts ServerGetMetricsOpts for k, vs := range u.Query() { switch k { case "type": for _, v := range vs { opts.Types = append(opts.Types, ServerMetricType(v)) } case "start": if len(vs) != 1 { t.Errorf("expected one value for start; got %d: %v", len(vs), vs) continue } v, err := time.Parse(time.RFC3339, vs[0]) if err != nil { t.Errorf("parse start as RFC3339: %v", err) } opts.Start = v case "end": if len(vs) != 1 { t.Errorf("expected one value for end; got %d: %v", len(vs), vs) continue } v, err := time.Parse(time.RFC3339, vs[0]) if err != nil { t.Errorf("parse end as RFC3339: %v", err) } opts.End = v case "step": if len(vs) != 1 { t.Errorf("expected one value for step; got %d: %v", len(vs), vs) continue } v, err := strconv.Atoi(vs[0]) if err != nil { t.Errorf("invalid step: %v", err) } opts.Step = v } } return opts } func TestServerAddToPlacementGroup(t *testing.T) { env := newTestEnv() defer env.Teardown() const ( serverID = 1 actionID = 42 placementGroupID = 123 ) env.Mux.HandleFunc(fmt.Sprintf("/servers/%d/actions/add_to_placement_group", serverID), func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionAddToPlacementGroupRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.PlacementGroup != placementGroupID { t.Errorf("unexpected PlacementGroup: %v", reqBody.PlacementGroup) } json.NewEncoder(w).Encode(schema.ServerActionAddToPlacementGroupResponse{ Action: schema.Action{ ID: actionID, }, }) }) var ( ctx = context.Background() server = &Server{ID: serverID} placementGroup = &PlacementGroup{ID: placementGroupID} ) action, _, err := env.Client.Server.AddToPlacementGroup(ctx, server, placementGroup) if err != nil { t.Fatal(err) } if action.ID != actionID { t.Errorf("unexpected action ID: %v", action.ID) } } func TestServerRemoveFromPlacementGroup(t *testing.T) { env := newTestEnv() defer env.Teardown() const ( serverID = 1 actionID = 42 ) env.Mux.HandleFunc(fmt.Sprintf("/servers/%d/actions/remove_from_placement_group", serverID), func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } json.NewEncoder(w).Encode(schema.ServerActionRemoveFromPlacementGroupResponse{ Action: schema.Action{ ID: actionID, }, }) }) var ( ctx = context.Background() server = &Server{ID: serverID} ) action, _, err := env.Client.Server.RemoveFromPlacementGroup(ctx, server) if err != nil { t.Fatal(err) } if action.ID != actionID { t.Errorf("unexpected action ID: %v", action.ID) } } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/server_type.go000066400000000000000000000076641414442033200250770ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/server_type_test.go000066400000000000000000000124001414442033200261160ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestServerTypeClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerTypeGetResponse{ ServerType: schema.ServerType{ ID: 1, }, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no server type") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } t.Run("via Get", func(t *testing.T) { serverType, _, err := env.Client.ServerType.Get(ctx, "1") if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no server type") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if serverType != nil { t.Fatal("expected no server type") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=cx10" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerTypeListResponse{ ServerTypes: []schema.ServerType{ { ID: 1, }, }, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByName(ctx, "cx10") if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no server type") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } t.Run("via Get", func(t *testing.T) { serverType, _, err := env.Client.ServerType.Get(ctx, "cx10") if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no serverType") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=cx10" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerTypeListResponse{ ServerTypes: []schema.ServerType{}, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByName(ctx, "cx10") if err != nil { t.Fatal(err) } if serverType != nil { t.Fatal("unexpected server type") } }) t.Run("GetByName (empty)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByName(ctx, "") if err != nil { t.Fatal(err) } if serverType != nil { t.Fatal("unexpected server type") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ServerTypeListResponse{ ServerTypes: []schema.ServerType{ {ID: 1}, {ID: 2}, }, }) }) opts := ServerTypeListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() serverTypes, _, err := env.Client.ServerType.List(ctx, opts) if err != nil { t.Fatal(err) } if len(serverTypes) != 2 { t.Fatal("expected 2 server types") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { ServerTypes []schema.ServerType `json:"server_types"` Meta schema.Meta `json:"meta"` }{ ServerTypes: []schema.ServerType{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 3, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() serverTypes, err := env.Client.ServerType.All(ctx) if err != nil { t.Fatalf("ServerTypes.List failed: %s", err) } if len(serverTypes) != 3 { t.Fatalf("expected 3 server types; got %d", len(serverTypes)) } if serverTypes[0].ID != 1 || serverTypes[1].ID != 2 || serverTypes[2].ID != 3 { t.Errorf("unexpected server types") } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/ssh_key.go000066400000000000000000000141551414442033200241660ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/ssh_key_test.go000066400000000000000000000326241414442033200252260ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestSSHKeyClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "ssh_key": { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "created": "2017-08-16T17:29:14+00:00" } }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByID(ctx, 1) if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } t.Run("via Get", func(t *testing.T) { sshKey, _, err := env.Client.SSHKey.Get(ctx, "1") if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } }) } func TestSSHKeyClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByID(ctx, 1) if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey != nil { t.Fatal("expected no SSH key") } } func TestSSHKeyClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=My+Key" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "ssh_keys": [{ "id": 1, "name": "My Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt" }] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByName(ctx, "My Key") if err != nil { t.Fatalf("SSHKey.GetByName failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } t.Run("via Get", func(t *testing.T) { sshKey, _, err := env.Client.SSHKey.Get(ctx, "My Key") if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } }) } func TestSSHKeyClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=My+Key" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "ssh_keys": [] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByName(ctx, "My Key") if err != nil { t.Fatalf("SSHKey.GetByName failed: %s", err) } if sshKey != nil { t.Fatal("unexpected SSH key") } } func TestSSHKeyClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByName(ctx, "") if err != nil { t.Fatal(err) } if sshKey != nil { t.Fatal("unexpected SSH key") } } func TestSSHKeyClientGetByFingerprint(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("fingerprint") != "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0" { t.Fatal("missing or invalid fingerprint query") } fmt.Fprint(w, `{ "ssh_keys": [{ "id": 1, "name": "My Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "created": "2017-08-16T17:29:14+00:00" }] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByFingerprint(ctx, "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0") if err != nil { t.Fatalf("SSHKey.GetByFingerprint failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } } func TestSSHKeyClientGetByFingerprintNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("fingerprint") != "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0" { t.Fatal("missing or invalid fingerprint query") } fmt.Fprint(w, `{ "ssh_keys": [] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByFingerprint(ctx, "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0") if err != nil { t.Fatalf("SSHKey.GetByFingerprint failed: %s", err) } if sshKey != nil { t.Fatal("unexpected SSH key") } } func TestSSHKeyClientList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } fmt.Fprint(w, `{ "ssh_keys": [ { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "created": "2017-08-16T17:29:14+00:00" }, { "id": 2, "name": "Another key", "fingerprint": "c7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...XX", "created": "2017-08-16T17:29:14+00:00" } ] }`) }) opts := SSHKeyListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() sshKeys, _, err := env.Client.SSHKey.List(ctx, opts) if err != nil { t.Fatalf("SSHKey.List failed: %s", err) } if len(sshKeys) != 2 { t.Fatal("unexpected number of SSH keys") } if sshKeys[0].ID != 1 || sshKeys[1].ID != 2 { t.Fatalf("unexpected SSH key IDs: %d, %d", sshKeys[0].ID, sshKeys[1].ID) } } func TestSSHKeyAll(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { SSHKeys []schema.SSHKey `json:"ssh_keys"` Meta schema.Meta `json:"meta"` }{ SSHKeys: []schema.SSHKey{ { ID: 1, Name: "My key", Fingerprint: "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", }, { ID: 2, Name: "Another key", Fingerprint: "c7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...XX", }, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 2, TotalEntries: 2, }, }, }) }) ctx := context.Background() sshKeys, err := env.Client.SSHKey.All(ctx) if err != nil { t.Fatal(err) } if len(sshKeys) != 2 { t.Fatalf("expected 2 SSH keys; got %d", len(sshKeys)) } if sshKeys[0].ID != 1 || sshKeys[1].ID != 2 { t.Error("got wrong SSH keys") } } func TestSSHKeyAllWithOpts(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { SSHKeys []schema.SSHKey `json:"ssh_keys"` Meta schema.Meta `json:"meta"` }{ SSHKeys: []schema.SSHKey{ { ID: 1, Name: "My key", Fingerprint: "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", }, { ID: 2, Name: "Another key", Fingerprint: "c7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...XX", }, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 2, TotalEntries: 2, }, }, }) }) ctx := context.Background() opts := SSHKeyListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}} sshKeys, err := env.Client.SSHKey.AllWithOpts(ctx, opts) if err != nil { t.Fatal(err) } if len(sshKeys) != 2 { t.Fatalf("expected 2 SSH keys; got %d", len(sshKeys)) } if sshKeys[0].ID != 1 || sshKeys[1].ID != 2 { t.Error("got wrong SSH keys") } } func TestSSHKeyClientDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() sshKey = &SSHKey{ID: 1} ) _, err := env.Client.SSHKey.Delete(ctx, sshKey) if err != nil { t.Fatalf("SSHKey.Delete failed: %s", err) } } func TestSSHKeyClientCreate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "ssh_key": { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt" } }`) }) ctx := context.Background() opts := SSHKeyCreateOpts{ Name: "My key", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", } sshKey, _, err := env.Client.SSHKey.Create(ctx, opts) if err != nil { t.Fatalf("SSHKey.Get failed: %s", err) } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } } func TestSSHKeyClientCreateWithLabels(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.SSHKeyCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } fmt.Fprint(w, `{ "ssh_key": { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt" } }`) }) ctx := context.Background() opts := SSHKeyCreateOpts{ Name: "My key", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", Labels: map[string]string{"key": "value"}, } sshKey, _, err := env.Client.SSHKey.Create(ctx, opts) if err != nil { t.Fatalf("SSHKey.Get failed: %s", err) } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } } func TestSSHKeyClientUpdate(t *testing.T) { var ( ctx = context.Background() sshKey = &SSHKey{ID: 1} ) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.SSHKeyUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{ SSHKey: schema.SSHKey{ ID: 1, }, }) }) opts := SSHKeyUpdateOpts{ Name: "test", } updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts) if err != nil { t.Fatal(err) } if updatedSSHKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.SSHKeyUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{ SSHKey: schema.SSHKey{ ID: 1, }, }) }) opts := SSHKeyUpdateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, } updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts) if err != nil { t.Fatal(err) } if updatedSSHKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.SSHKeyUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{ SSHKey: schema.SSHKey{ ID: 1, }, }) }) opts := SSHKeyUpdateOpts{} updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts) if err != nil { t.Fatal(err) } if updatedSSHKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID) } }) } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/testing.go000066400000000000000000000005041414442033200241670ustar00rootroot00000000000000package hcloud import ( "testing" "time" ) const apiTimestampFormat = "2006-01-02T15:04:05-07:00" func mustParseTime(t *testing.T, layout, value string) time.Time { t.Helper() ts, err := time.Parse(layout, value) if err != nil { t.Fatalf("parse time: layout %v: value %v: %v", layout, value, err) } return ts } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/volume.go000066400000000000000000000253111414442033200240240ustar00rootroot00000000000000package 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 } golang-github-hetznercloud-hcloud-go-1.33.1/hcloud/volume_test.go000066400000000000000000000312501414442033200250620ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestVolumeClientGet(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "db-storage", "status": "creating", "server": null, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true } } }`) }) ctx := context.Background() t.Run("GetByID", func(t *testing.T) { volume, _, err := env.Client.Volume.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) t.Run("Get", func(t *testing.T) { volume, _, err := env.Client.Volume.Get(ctx, "1") if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) } func TestVolumeClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() volume, _, err := env.Client.Volume.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if volume != nil { t.Fatal("expected no volume") } } func TestVolumeClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my-volume" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "volumes": [ { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "status": "creating", "server": null, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true } } ] }`) }) ctx := context.Background() t.Run("GetByName", func(t *testing.T) { volume, _, err := env.Client.Volume.GetByName(ctx, "my-volume") if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) t.Run("Get", func(t *testing.T) { volume, _, err := env.Client.Volume.Get(ctx, "my-volume") if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) } func TestVolumeClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my-volume" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "volumes": [] }`) }) ctx := context.Background() volume, _, err := env.Client.Volume.GetByName(ctx, "my-volume") if err != nil { t.Fatal(err) } if volume != nil { t.Fatal("unexpected volume") } } func TestVolumeClientGetByNameEmpty(t *testing.T) { env := newTestEnv() defer env.Teardown() ctx := context.Background() volume, _, err := env.Client.Volume.GetByName(ctx, "") if err != nil { t.Fatal(err) } if volume != nil { t.Fatal("unexpected volume") } } func TestVolumeClientDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "DELETE" { t.Error("expected DELETE") } }) var ( ctx = context.Background() volume = &Volume{ID: 1} ) _, err := env.Client.Volume.Delete(ctx, volume) if err != nil { t.Fatal(err) } } func TestVolumeClientCreateWithServer(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "status": "creating", "server": 1, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": {} }, "action": { "id": 2, "command": "create_volume", "status": "running", "progress": 0, "started": "2016-01-30T23:50:11+00:00", "finished": null, "resources": [ { "id": 42, "type": "server" }, { "id": 1, "type": "volume" } ] }, "next_actions": [ { "id": 3, "command": "attach_volume", "status": "running", "progress": 0, "started": "2016-01-30T23:50:15+00:00", "finished": null, "resources": [ { "id": 42, "type": "server" }, { "id": 1, "type": "volume" } ] } ] }`) }) ctx := context.Background() opts := VolumeCreateOpts{ Name: "my-volume", Size: 42, Server: &Server{ID: 1}, } result, _, err := env.Client.Volume.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.Volume.ID != 1 { t.Errorf("unexpected volume ID: %v", result.Volume.ID) } if result.Action.ID != 2 { t.Errorf("unexpected action ID: %v", result.Action.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 3 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestVolumeClientCreateWithLocation(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.VolumeCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "my-volume" { t.Errorf("unexpected volume name in request: %v", reqBody.Name) } if reqBody.Size != 42 { t.Errorf("unexpected volume size in request: %v", reqBody.Size) } if reqBody.Location != float64(1) { t.Errorf("unexpected volume location in request: %v", reqBody.Location) } if reqBody.Server != nil { t.Errorf("unexpected server in request: %v", reqBody.Server) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } if reqBody.Automount != nil { t.Errorf("unexpected automount in request: %v", reqBody.Automount) } if reqBody.Format != nil { t.Errorf("unexpected format in request: %v", reqBody.Automount) } fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "server": null, "status": "creating", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": { "key": "value" } } }`) }) ctx := context.Background() opts := VolumeCreateOpts{ Name: "my-volume", Size: 42, Location: &Location{ID: 1}, Labels: map[string]string{"key": "value"}, } result, _, err := env.Client.Volume.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.Volume.ID != 1 { t.Errorf("unexpected volume ID: %v", result.Volume.ID) } } func TestVolumeClientCreateWithAutomount(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.VolumeCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "my-volume" { t.Errorf("unexpected volume name in request: %v", reqBody.Name) } if reqBody.Size != 42 { t.Errorf("unexpected volume size in request: %v", reqBody.Size) } if reqBody.Server == nil || *reqBody.Server != 1 { t.Errorf("unexpected server in request: %v", reqBody.Server) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } if *reqBody.Automount != true { t.Errorf("unexpected automount in request: %v", reqBody.Automount) } if *reqBody.Format != "xfs" { t.Errorf("unexpected format in request: %v", reqBody.Automount) } fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "status": "creating", "server": 1, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": { "key": "value" } } }`) }) ctx := context.Background() opts := VolumeCreateOpts{ Name: "my-volume", Size: 42, Server: &Server{ID: 1}, Labels: map[string]string{"key": "value"}, Automount: Bool(true), Format: String("xfs"), } result, _, err := env.Client.Volume.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.Volume.ID != 1 { t.Errorf("unexpected volume ID: %v", result.Volume.ID) } } func TestVolumeClientUpdate(t *testing.T) { var ( ctx = context.Background() volume = &Volume{ID: 1} ) t.Run("name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.VolumeUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.VolumeUpdateResponse{ Volume: schema.Volume{ ID: 1, }, }) }) opts := VolumeUpdateOpts{ Name: "test", } updatedVolume, _, err := env.Client.Volume.Update(ctx, volume, opts) if err != nil { t.Fatal(err) } if updatedVolume.ID != 1 { t.Errorf("unexpected volume ID: %v", updatedVolume.ID) } }) t.Run("labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.VolumeUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.VolumeUpdateResponse{ Volume: schema.Volume{ ID: 1, }, }) }) opts := VolumeUpdateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, } updatedVolume, _, err := env.Client.Volume.Update(ctx, volume, opts) if err != nil { t.Fatal(err) } if updatedVolume.ID != 1 { t.Errorf("unexpected volume ID: %v", updatedVolume.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.VolumeUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } if reqBody.Labels != nil { t.Errorf("unexpected no labels, but got: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.VolumeUpdateResponse{ Volume: schema.Volume{ ID: 1, }, }) }) opts := VolumeUpdateOpts{} updatedVolume, _, err := env.Client.Volume.Update(ctx, volume, opts) if err != nil { t.Fatal(err) } if updatedVolume.ID != 1 { t.Errorf("unexpected volume ID: %v", updatedVolume.ID) } }) } golang-github-hetznercloud-hcloud-go-1.33.1/script/000077500000000000000000000000001414442033200222125ustar00rootroot00000000000000golang-github-hetznercloud-hcloud-go-1.33.1/script/checkall.bash000077500000000000000000000001601414442033200246170ustar00rootroot00000000000000#!/bin/bash -e cd hcloud diff -u <(echo -n) <(gofmt -d -s .) go vet ./... golint -set_exit_status go test ./...