├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ ├── releaser-pleaser.yml │ └── stale.yml ├── .gitignore ├── .gitlab-ci.yml ├── .gitlab └── CODEOWNERS ├── .golangci.yaml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── codecov.yml ├── go.mod ├── go.sum ├── hcloud ├── action.go ├── action_test.go ├── action_waiter.go ├── action_waiter_test.go ├── action_watch.go ├── action_watch_test.go ├── architecture.go ├── certificate.go ├── certificate_test.go ├── client.go ├── client_generic.go ├── client_generic_test.go ├── client_handler.go ├── client_handler_debug.go ├── client_handler_debug_test.go ├── client_handler_error.go ├── client_handler_error_test.go ├── client_handler_http.go ├── client_handler_http_test.go ├── client_handler_parse.go ├── client_handler_parse_test.go ├── client_handler_rate_limit.go ├── client_handler_rate_limit_test.go ├── client_handler_retry.go ├── client_handler_retry_test.go ├── client_handler_test.go ├── client_helper.go ├── client_helper_test.go ├── client_test.go ├── datacenter.go ├── datacenter_test.go ├── deprecation.go ├── deprecation_test.go ├── error.go ├── error_test.go ├── exp │ ├── README.md │ ├── actionutil │ │ ├── actions.go │ │ └── actions_test.go │ ├── ctxutil │ │ ├── ctxutil.go │ │ └── ctxutil_test.go │ ├── doc.go │ ├── kit │ │ ├── envutil │ │ │ ├── env.go │ │ │ └── env_test.go │ │ ├── randutil │ │ │ ├── id.go │ │ │ └── id_test.go │ │ └── sshutil │ │ │ ├── ssh_key.go │ │ │ └── ssh_key_test.go │ ├── labelutil │ │ ├── selector.go │ │ └── selector_test.go │ └── mockutil │ │ ├── http.go │ │ └── http_test.go ├── firewall.go ├── firewall_test.go ├── floating_ip.go ├── floating_ip_test.go ├── hcloud.go ├── hcloud_test.go ├── helper.go ├── image.go ├── image_test.go ├── interface_gen.go ├── internal │ └── instrumentation │ │ ├── metrics.go │ │ └── metrics_test.go ├── iso.go ├── iso_test.go ├── labels.go ├── labels_test.go ├── load_balancer.go ├── load_balancer_test.go ├── load_balancer_type.go ├── load_balancer_type_test.go ├── location.go ├── location_test.go ├── metadata │ ├── client.go │ └── client_test.go ├── mocked_test.go ├── network.go ├── network_test.go ├── placement_group.go ├── placement_group_test.go ├── pricing.go ├── pricing_test.go ├── primary_ip.go ├── primary_ip_test.go ├── rdns.go ├── rdns_test.go ├── resource.go ├── schema.go ├── schema │ ├── README.md │ ├── action.go │ ├── certificate.go │ ├── datacenter.go │ ├── deprecation.go │ ├── doc.go │ ├── error.go │ ├── error_test.go │ ├── firewall.go │ ├── floating_ip.go │ ├── floating_ip_test.go │ ├── id_or_name.go │ ├── id_or_name_test.go │ ├── image.go │ ├── image_test.go │ ├── iso.go │ ├── load_balancer.go │ ├── load_balancer_type.go │ ├── location.go │ ├── meta.go │ ├── network.go │ ├── network_test.go │ ├── placement_group.go │ ├── pricing.go │ ├── primary_ip.go │ ├── server.go │ ├── server_test.go │ ├── server_type.go │ ├── ssh_key.go │ ├── ssh_key_test.go │ ├── volume.go │ └── volume_test.go ├── schema_assign.go ├── schema_gen.go ├── schema_test.go ├── server.go ├── server_test.go ├── server_type.go ├── server_type_test.go ├── ssh_key.go ├── ssh_key_test.go ├── testing.go ├── volume.go ├── volume_test.go ├── zz_action_client_iface.go ├── zz_certificate_client_iface.go ├── zz_datacenter_client_iface.go ├── zz_firewall_client_iface.go ├── zz_floating_ip_client_iface.go ├── zz_image_client_iface.go ├── zz_iso_client_iface.go ├── zz_load_balancer_client_iface.go ├── zz_load_balancer_type_client_iface.go ├── zz_location_client_iface.go ├── zz_network_client_iface.go ├── zz_placement_group_client_iface.go ├── zz_pricing_client_iface.go ├── zz_primary_ip_client_iface.go ├── zz_rdns_client_iface.go ├── zz_resource_action_client_iface.go ├── zz_schema.go ├── zz_server_client_iface.go ├── zz_server_type_client_iface.go ├── zz_ssh_key_client_iface.go └── zz_volume_client_iface.go ├── renovate.json └── tools.go /.gitattributes: -------------------------------------------------------------------------------- 1 | hcloud/zz_*.go linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hetznercloud/integrations 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | pre-commit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: 3.x 18 | 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: "1.24" 22 | 23 | - uses: pre-commit/action@v3.0.1 24 | 25 | test: 26 | strategy: 27 | matrix: 28 | go-version: 29 | - "1.23" 30 | - "1.24" 31 | 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: actions/setup-go@v5 36 | with: 37 | go-version: ${{ matrix.go-version }} 38 | 39 | - run: go test -coverprofile=coverage.txt -v -race ./... 40 | 41 | - uses: codecov/codecov-action@v5 42 | if: > 43 | !startsWith(github.head_ref, 'renovate/') && 44 | !startsWith(github.head_ref, 'releaser-pleaser--') 45 | with: 46 | token: ${{ secrets.CODECOV_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/releaser-pleaser.yml: -------------------------------------------------------------------------------- 1 | name: Releaser-pleaser 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request_target: 7 | types: 8 | - edited 9 | - labeled 10 | - unlabeled 11 | 12 | jobs: 13 | releaser-pleaser: 14 | # Do not run on forks. 15 | if: github.repository == 'hetznercloud/hcloud-go' 16 | 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: releaser-pleaser 20 | uses: apricote/releaser-pleaser@v0.5.1 21 | with: 22 | extra-files: | 23 | hcloud/hcloud.go 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: "30 12 * * *" 6 | 7 | jobs: 8 | stale: 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | 13 | uses: hetznercloud/.github/.github/workflows/stale.yml@main 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage.txt 3 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project: cloud/integrations/ci 3 | file: 4 | - default.yml 5 | - workflows/feature-branches.yml 6 | - pre-commit.yml 7 | 8 | stages: 9 | - test 10 | 11 | pre-commit: 12 | extends: [.pre-commit] 13 | 14 | test: 15 | stage: test 16 | image: golang:1.24 17 | variables: 18 | GOMODCACHE: $CI_PROJECT_DIR/.cache/go-mod 19 | cache: 20 | key: 21 | files: [go.mod, go.sum] 22 | paths: [$GOMODCACHE] 23 | before_script: 24 | - go get github.com/boumenot/gocover-cobertura@latest 25 | - go mod download 26 | script: 27 | - go test -coverprofile=coverage.txt -v -race ./... 28 | - mv coverage.txt coverage.tmp; grep -v '/zz_' coverage.tmp > coverage.txt; rm coverage.tmp 29 | - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml 30 | - go tool cover -func=coverage.txt 31 | artifacts: 32 | paths: 33 | - coverage.txt 34 | reports: 35 | coverage_report: 36 | coverage_format: cobertura 37 | path: coverage.xml 38 | coverage: /total:\s+\(statements\)\s+\d+.\d+%/ 39 | -------------------------------------------------------------------------------- /.gitlab/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cloud/integrations 2 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | presets: 4 | - bugs 5 | - error 6 | - import 7 | - metalinter 8 | - module 9 | - unused 10 | 11 | enable: 12 | - forbidigo 13 | - testifylint 14 | 15 | disable: 16 | # preset error 17 | - err113 # Very annoying to define static errors everywhere 18 | - wrapcheck # Very annoying to wrap errors everywhere 19 | # preset import 20 | - depguard 21 | 22 | linters-settings: 23 | gci: 24 | sections: 25 | - standard 26 | - default 27 | - prefix(github.com/hetznercloud) 28 | 29 | exhaustive: 30 | # Switch cases with a default case should be exhaustive. 31 | default-signifies-exhaustive: true 32 | 33 | issues: 34 | exclude-rules: 35 | - path: _test\.go 36 | linters: 37 | - errcheck 38 | - gosec 39 | - noctx 40 | 41 | - path: _test\.go 42 | linters: 43 | - revive 44 | text: "unused-parameter: parameter '(w|r)' seems to be unused, consider removing or renaming it as _" 45 | 46 | - linters: 47 | - testifylint 48 | text: "require-error" 49 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://pre-commit.com for more information 3 | # See https://pre-commit.com/hooks.html for more hooks 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v5.0.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-case-conflict 10 | - id: check-executables-have-shebangs 11 | - id: check-shebang-scripts-are-executable 12 | - id: check-symlinks 13 | - id: destroyed-symlinks 14 | 15 | - id: check-json 16 | - id: check-yaml 17 | - id: check-toml 18 | 19 | - id: check-merge-conflict 20 | - id: end-of-file-fixer 21 | - id: mixed-line-ending 22 | args: [--fix=lf] 23 | - id: trailing-whitespace 24 | 25 | - repo: local 26 | hooks: 27 | - id: prettier 28 | name: prettier 29 | language: node 30 | additional_dependencies: [prettier@3.3.3] 31 | entry: prettier --write --ignore-unknown 32 | types: [text] 33 | require_serial: false 34 | files: \.(md|ya?ml)$ 35 | 36 | - repo: local 37 | hooks: 38 | - id: go-mod-tidy 39 | name: go mod tidy 40 | language: golang 41 | entry: go mod tidy 42 | pass_filenames: false 43 | 44 | - id: go-generate 45 | name: go generate 46 | language: golang 47 | entry: go generate ./... 48 | pass_filenames: false 49 | 50 | - repo: https://github.com/golangci/golangci-lint 51 | rev: v1.64.8 52 | hooks: 53 | - id: golangci-lint-full 54 | args: [--timeout=5m] 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 Hetzner Cloud GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This guide is intended for _maintainers_. 4 | 5 | ## Release Branches 6 | 7 | All development targets the `main` branch. New releases for the latest major version series are cut from this branch. 8 | 9 | For the older major versions, we also have `release-.x` branches where we try to backport all bug fixes. 10 | 11 | Backports are done by [tibdex/backport](https://github.com/tibdex/backport). Apply the label `backport release-.x` to the PRs and once they are merged a new PR is opened. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hcloud: A Go library for the Hetzner Cloud API 2 | 3 | [![GitHub Actions status](https://github.com/hetznercloud/hcloud-go/workflows/Continuous%20Integration/badge.svg)](https://github.com/hetznercloud/hcloud-go/actions) 4 | [![Codecov](https://codecov.io/github/hetznercloud/hcloud-go/graph/badge.svg?token=4IAbGIwNYp)](https://codecov.io/github/hetznercloud/hcloud-go/tree/main) 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/hetznercloud/hcloud-go/v2/hcloud.svg)](https://pkg.go.dev/github.com/hetznercloud/hcloud-go/v2/hcloud) 6 | 7 | Package hcloud is a library for the Hetzner Cloud API. 8 | 9 | The library’s documentation is available at [pkg.go.dev](https://godoc.org/github.com/hetznercloud/hcloud-go/v2/hcloud), 10 | the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud/). 11 | 12 | > [!IMPORTANT] 13 | > Make sure to follow our API changelog available at 14 | > [docs.hetzner.cloud/changelog](https://docs.hetzner.cloud/changelog) (or the RSS feed 15 | > available at 16 | > [docs.hetzner.cloud/changelog/feed.rss](https://docs.hetzner.cloud/changelog/feed.rss)) 17 | > to be notified about additions, deprecations and removals. 18 | 19 | ## Installation 20 | 21 | ```sh 22 | go get github.com/hetznercloud/hcloud-go/v2/hcloud 23 | ``` 24 | 25 | ## Example 26 | 27 | ```go 28 | package main 29 | 30 | import ( 31 | "context" 32 | "fmt" 33 | "log" 34 | 35 | "github.com/hetznercloud/hcloud-go/v2/hcloud" 36 | ) 37 | 38 | func main() { 39 | client := hcloud.NewClient(hcloud.WithToken("token")) 40 | 41 | server, _, err := client.Server.GetByID(context.Background(), 1) 42 | if err != nil { 43 | log.Fatalf("error retrieving server: %s\n", err) 44 | } 45 | if server != nil { 46 | fmt.Printf("server 1 is called %q\n", server.Name) 47 | } else { 48 | fmt.Println("server 1 not found") 49 | } 50 | } 51 | ``` 52 | 53 | ## Upgrading 54 | 55 | ### Support 56 | 57 | - `v2` is actively maintained by Hetzner Cloud 58 | - `v1` is unsupported since February 2025. 59 | 60 | ### From v1 to v2 61 | 62 | Version 2.0.0 was published because we changed the datatype of all `ID` fields from `int` to `int64`. 63 | 64 | To migrate to the new version, replace all your imports to reference the new module path: 65 | 66 | ```diff 67 | import ( 68 | - "github.com/hetznercloud/hcloud-go/hcloud" 69 | + "github.com/hetznercloud/hcloud-go/v2/hcloud" 70 | ) 71 | ``` 72 | 73 | When you compile your code, it will show any invalid usages of `int` in your code that you need to fix. We commonly found these changes while updating our integrations: 74 | 75 | - `strconv.Atoi(idString)` (parsing integers) needs to be replaced by `strconv.ParseInt(idString, 10, 64)` 76 | - `strconv.Itoa(id)` (formatting integers) needs to be replaced by `strconv.FormatInt(id, 10)` 77 | 78 | ## Go Version Support 79 | 80 | The library supports the latest two Go minor versions, e.g. at the time Go 1.19 is released, it supports Go 1.18 and 1.19. 81 | 82 | This matches the official [Go Release Policy](https://go.dev/doc/devel/release#policy). 83 | 84 | When the minimum required Go version is changed, it is announced in the release notes for that version. 85 | 86 | ## License 87 | 88 | MIT license 89 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "**/zz_*.go" 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hetznercloud/hcloud-go/v2 2 | 3 | // This is being kept at a lower value on purpose as raising this would require 4 | // all dependends to update to the new version. 5 | // As long as we do not depend on any newer language feature this can be kept at the current value. 6 | // It should never be higher than the lowest currently supported version of Go. 7 | // Since golang.org/x dependencies always requires version 1.(N-1), this is effectively 8 | // the same version we will be using. (See http://go.dev/issue/69095) 9 | go 1.23.0 10 | 11 | // The toolchain version describes which Go version to use for testing, generating etc. 12 | // It should always be the newest version. 13 | toolchain go1.24.4 14 | 15 | require ( 16 | github.com/google/go-cmp v0.7.0 17 | github.com/jmattheis/goverter v1.8.3 18 | github.com/prometheus/client_golang v1.22.0 19 | github.com/stretchr/testify v1.10.0 20 | github.com/vburenin/ifacemaker v1.3.0 21 | golang.org/x/crypto v0.38.0 22 | golang.org/x/net v0.40.0 23 | ) 24 | 25 | require ( 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 28 | github.com/dave/jennifer v1.6.0 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/jessevdk/go-flags v1.6.1 // indirect 31 | github.com/kr/text v0.2.0 // indirect 32 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/prometheus/client_model v0.6.1 // indirect 35 | github.com/prometheus/common v0.62.0 // indirect 36 | github.com/prometheus/procfs v0.15.1 // indirect 37 | github.com/rogpeppe/go-internal v1.11.0 // indirect 38 | golang.org/x/mod v0.24.0 // indirect 39 | golang.org/x/sync v0.14.0 // indirect 40 | golang.org/x/sys v0.33.0 // indirect 41 | golang.org/x/text v0.25.0 // indirect 42 | golang.org/x/tools v0.31.0 // indirect 43 | google.golang.org/protobuf v1.36.5 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /hcloud/action_waiter.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "maps" 7 | "slices" 8 | "time" 9 | ) 10 | 11 | type ActionWaiter interface { 12 | WaitForFunc(ctx context.Context, handleUpdate func(update *Action) error, actions ...*Action) error 13 | WaitFor(ctx context.Context, actions ...*Action) error 14 | } 15 | 16 | var _ ActionWaiter = (*ActionClient)(nil) 17 | 18 | // WaitForFunc waits until all actions are completed by polling the API at the interval 19 | // defined by [WithPollOpts]. An action is considered as complete when its status is 20 | // either [ActionStatusSuccess] or [ActionStatusError]. 21 | // 22 | // The handleUpdate callback is called every time an action is updated. 23 | func (c *ActionClient) WaitForFunc(ctx context.Context, handleUpdate func(update *Action) error, actions ...*Action) error { 24 | // Filter out nil actions 25 | actions = slices.DeleteFunc(actions, func(a *Action) bool { return a == nil }) 26 | 27 | running := make(map[int64]struct{}, len(actions)) 28 | for _, action := range actions { 29 | if action.Status == ActionStatusRunning { 30 | running[action.ID] = struct{}{} 31 | } else if handleUpdate != nil { 32 | // We filter out already completed actions from the API polling loop; while 33 | // this isn't a real update, the caller should be notified about the new 34 | // state. 35 | if err := handleUpdate(action); err != nil { 36 | return err 37 | } 38 | } 39 | } 40 | 41 | retries := 0 42 | for { 43 | if len(running) == 0 { 44 | break 45 | } 46 | 47 | select { 48 | case <-ctx.Done(): 49 | return ctx.Err() 50 | case <-time.After(c.action.client.pollBackoffFunc(retries)): 51 | retries++ 52 | } 53 | 54 | updates := make([]*Action, 0, len(running)) 55 | for runningIDsChunk := range slices.Chunk(slices.Sorted(maps.Keys(running)), 25) { 56 | opts := ActionListOpts{ 57 | Sort: []string{"status", "id"}, 58 | ID: runningIDsChunk, 59 | } 60 | 61 | updatesChunk, err := c.AllWithOpts(ctx, opts) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | updates = append(updates, updatesChunk...) 67 | } 68 | 69 | if len(updates) != len(running) { 70 | // Some actions may not exist in the API, also fail early to prevent an 71 | // infinite loop when updates == 0. 72 | 73 | notFound := maps.Clone(running) 74 | for _, update := range updates { 75 | delete(notFound, update.ID) 76 | } 77 | notFoundIDs := make([]int64, 0, len(notFound)) 78 | for unknownID := range notFound { 79 | notFoundIDs = append(notFoundIDs, unknownID) 80 | } 81 | 82 | return fmt.Errorf("actions not found: %v", notFoundIDs) 83 | } 84 | 85 | for _, update := range updates { 86 | if update.Status != ActionStatusRunning { 87 | delete(running, update.ID) 88 | } 89 | 90 | if handleUpdate != nil { 91 | if err := handleUpdate(update); err != nil { 92 | return err 93 | } 94 | } 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // WaitFor waits until all actions succeed by polling the API at the interval defined by 102 | // [WithPollOpts]. An action is considered as succeeded when its status is either 103 | // [ActionStatusSuccess]. 104 | // 105 | // If a single action fails, the function will stop waiting and the error set in the 106 | // action will be returned as an [ActionError]. 107 | // 108 | // For more flexibility, see the [ActionClient.WaitForFunc] function. 109 | func (c *ActionClient) WaitFor(ctx context.Context, actions ...*Action) error { 110 | return c.WaitForFunc( 111 | ctx, 112 | func(update *Action) error { 113 | if update.Status == ActionStatusError { 114 | return update.Error() 115 | } 116 | return nil 117 | }, 118 | actions..., 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /hcloud/action_watch.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // WatchOverallProgress watches several actions' progress until they complete 9 | // with success or error. This watching happens in a goroutine and updates are 10 | // provided through the two returned channels: 11 | // 12 | // - The first channel receives percentage updates of the progress, based on 13 | // the number of completed versus total watched actions. The return value 14 | // is an int between 0 and 100. 15 | // - The second channel returned receives errors for actions that did not 16 | // complete successfully, as well as any errors that happened while 17 | // querying the API. 18 | // 19 | // By default, the method keeps watching until all actions have finished 20 | // processing. If you want to be able to cancel the method or configure a 21 | // timeout, use the [context.Context]. Once the method has stopped watching, 22 | // both returned channels are closed. 23 | // 24 | // WatchOverallProgress uses the [WithPollOpts] of the [Client] to wait 25 | // until sending the next request. 26 | // 27 | // Deprecated: WatchOverallProgress is deprecated, use [WaitForFunc] instead. 28 | func (c *ActionClient) WatchOverallProgress(ctx context.Context, actions []*Action) (<-chan int, <-chan error) { 29 | errCh := make(chan error, len(actions)) 30 | progressCh := make(chan int) 31 | 32 | go func() { 33 | defer close(errCh) 34 | defer close(progressCh) 35 | 36 | previousGlobalProgress := 0 37 | progressByAction := make(map[int64]int, len(actions)) 38 | err := c.WaitForFunc(ctx, func(update *Action) error { 39 | switch update.Status { 40 | case ActionStatusRunning: 41 | progressByAction[update.ID] = update.Progress 42 | case ActionStatusSuccess: 43 | progressByAction[update.ID] = 100 44 | case ActionStatusError: 45 | progressByAction[update.ID] = 100 46 | errCh <- fmt.Errorf("action %d failed: %w", update.ID, update.Error()) 47 | } 48 | 49 | // Compute global progress 50 | progressSum := 0 51 | for _, value := range progressByAction { 52 | progressSum += value 53 | } 54 | globalProgress := progressSum / len(actions) 55 | 56 | // Only send progress when it changed 57 | if globalProgress != 0 && globalProgress != previousGlobalProgress { 58 | sendProgress(progressCh, globalProgress) 59 | previousGlobalProgress = globalProgress 60 | } 61 | 62 | return nil 63 | }, actions...) 64 | 65 | if err != nil { 66 | errCh <- err 67 | } 68 | }() 69 | 70 | return progressCh, errCh 71 | } 72 | 73 | // WatchProgress watches one action's progress until it completes with success 74 | // or error. This watching happens in a goroutine and updates are provided 75 | // through the two returned channels: 76 | // 77 | // - The first channel receives percentage updates of the progress, based on 78 | // the progress percentage indicated by the API. The return value is an int 79 | // between 0 and 100. 80 | // - The second channel receives any errors that happened while querying the 81 | // API, as well as the error of the action if it did not complete 82 | // successfully, or nil if it did. 83 | // 84 | // By default, the method keeps watching until the action has finished 85 | // processing. If you want to be able to cancel the method or configure a 86 | // timeout, use the [context.Context]. Once the method has stopped watching, 87 | // both returned channels are closed. 88 | // 89 | // WatchProgress uses the [WithPollOpts] of the [Client] to wait until 90 | // sending the next request. 91 | // 92 | // Deprecated: WatchProgress is deprecated, use [WaitForFunc] instead. 93 | func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) { 94 | errCh := make(chan error, 1) 95 | progressCh := make(chan int) 96 | 97 | go func() { 98 | defer close(errCh) 99 | defer close(progressCh) 100 | 101 | err := c.WaitForFunc(ctx, func(update *Action) error { 102 | switch update.Status { 103 | case ActionStatusRunning: 104 | sendProgress(progressCh, update.Progress) 105 | case ActionStatusSuccess: 106 | sendProgress(progressCh, 100) 107 | case ActionStatusError: 108 | // Do not wrap the action error 109 | return update.Error() 110 | } 111 | 112 | return nil 113 | }, action) 114 | 115 | if err != nil { 116 | errCh <- err 117 | } 118 | }() 119 | 120 | return progressCh, errCh 121 | } 122 | 123 | // sendProgress allows the user to only read from the error channel and ignore any progress updates. 124 | func sendProgress(progressCh chan int, p int) { 125 | select { 126 | case progressCh <- p: 127 | break 128 | default: 129 | break 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /hcloud/architecture.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | // Architecture specifies the architecture of the CPU. 4 | type Architecture string 5 | 6 | const ( 7 | // ArchitectureX86 is the architecture for Intel/AMD x86 CPUs. 8 | ArchitectureX86 Architecture = "x86" 9 | 10 | // ArchitectureARM is the architecture for ARM CPUs. 11 | ArchitectureARM Architecture = "arm" 12 | ) 13 | -------------------------------------------------------------------------------- /hcloud/client_generic.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | ) 9 | 10 | func getRequest[Schema any](ctx context.Context, client *Client, url string) (Schema, *Response, error) { 11 | var respBody Schema 12 | 13 | req, err := client.NewRequest(ctx, "GET", url, nil) 14 | if err != nil { 15 | return respBody, nil, err 16 | } 17 | 18 | resp, err := client.Do(req, &respBody) 19 | if err != nil { 20 | return respBody, resp, err 21 | } 22 | 23 | return respBody, resp, nil 24 | } 25 | 26 | func postRequest[Schema any](ctx context.Context, client *Client, url string, reqBody any) (Schema, *Response, error) { 27 | var respBody Schema 28 | 29 | var reqBodyReader io.Reader 30 | if reqBody != nil { 31 | reqBodyBytes, err := json.Marshal(reqBody) 32 | if err != nil { 33 | return respBody, nil, err 34 | } 35 | 36 | reqBodyReader = bytes.NewReader(reqBodyBytes) 37 | } 38 | 39 | req, err := client.NewRequest(ctx, "POST", url, reqBodyReader) 40 | if err != nil { 41 | return respBody, nil, err 42 | } 43 | 44 | resp, err := client.Do(req, &respBody) 45 | if err != nil { 46 | return respBody, resp, err 47 | } 48 | 49 | return respBody, resp, nil 50 | } 51 | 52 | func putRequest[Schema any](ctx context.Context, client *Client, url string, reqBody any) (Schema, *Response, error) { 53 | var respBody Schema 54 | 55 | var reqBodyReader io.Reader 56 | if reqBody != nil { 57 | reqBodyBytes, err := json.Marshal(reqBody) 58 | if err != nil { 59 | return respBody, nil, err 60 | } 61 | 62 | reqBodyReader = bytes.NewReader(reqBodyBytes) 63 | } 64 | 65 | req, err := client.NewRequest(ctx, "PUT", url, reqBodyReader) 66 | if err != nil { 67 | return respBody, nil, err 68 | } 69 | 70 | resp, err := client.Do(req, &respBody) 71 | if err != nil { 72 | return respBody, resp, err 73 | } 74 | 75 | return respBody, resp, nil 76 | } 77 | 78 | func deleteRequest[Schema any](ctx context.Context, client *Client, url string) (Schema, *Response, error) { 79 | var respBody Schema 80 | 81 | req, err := client.NewRequest(ctx, "DELETE", url, nil) 82 | if err != nil { 83 | return respBody, nil, err 84 | } 85 | 86 | resp, err := client.Do(req, &respBody) 87 | if err != nil { 88 | return respBody, resp, err 89 | } 90 | 91 | return respBody, resp, nil 92 | } 93 | 94 | func deleteRequestNoResult(ctx context.Context, client *Client, url string) (*Response, error) { 95 | req, err := client.NewRequest(ctx, "DELETE", url, nil) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | return client.Do(req, nil) 101 | } 102 | -------------------------------------------------------------------------------- /hcloud/client_generic_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 11 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/mockutil" 12 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 13 | ) 14 | 15 | func TestGenericRequest(t *testing.T) { 16 | t.Run("GET", func(t *testing.T) { 17 | ctx, server, client := makeTestUtils(t) 18 | 19 | server.Expect([]mockutil.Request{ 20 | { 21 | Method: "GET", Path: "/resource", 22 | Status: 200, 23 | JSON: schema.ActionGetResponse{Action: schema.Action{ID: 1234}}, 24 | }, 25 | }) 26 | 27 | const opPath = "/resource" 28 | ctx = ctxutil.SetOpPath(ctx, opPath) 29 | 30 | respBody, resp, err := getRequest[schema.ActionGetResponse](ctx, client, opPath) 31 | require.NoError(t, err) 32 | require.NotNil(t, resp) 33 | require.NotNil(t, respBody) 34 | 35 | require.Equal(t, int64(1234), respBody.Action.ID) 36 | }) 37 | 38 | t.Run("POST", func(t *testing.T) { 39 | ctx, server, client := makeTestUtils(t) 40 | 41 | server.Expect([]mockutil.Request{ 42 | { 43 | Method: "POST", Path: "/resource", 44 | Want: func(t *testing.T, r *http.Request) { 45 | bodyBytes, err := io.ReadAll(r.Body) 46 | require.NoError(t, err) 47 | require.JSONEq(t, `{"hello": "world"}`, string(bodyBytes)) 48 | }, 49 | Status: 200, 50 | JSON: schema.ActionGetResponse{Action: schema.Action{ID: 1234}}, 51 | }, 52 | }) 53 | 54 | const opPath = "/resource" 55 | ctx = ctxutil.SetOpPath(ctx, opPath) 56 | 57 | respBody, resp, err := postRequest[schema.ActionGetResponse](ctx, client, opPath, map[string]string{"hello": "world"}) 58 | require.NoError(t, err) 59 | require.NotNil(t, resp) 60 | require.NotNil(t, respBody) 61 | 62 | require.Equal(t, int64(1234), respBody.Action.ID) 63 | }) 64 | 65 | t.Run("PUT", func(t *testing.T) { 66 | ctx, server, client := makeTestUtils(t) 67 | 68 | server.Expect([]mockutil.Request{ 69 | { 70 | Method: "PUT", Path: "/resource", 71 | Want: func(t *testing.T, r *http.Request) { 72 | bodyBytes, err := io.ReadAll(r.Body) 73 | require.NoError(t, err) 74 | require.JSONEq(t, `{"hello": "world"}`, string(bodyBytes)) 75 | }, 76 | Status: 200, 77 | JSON: schema.ActionGetResponse{Action: schema.Action{ID: 1234}}, 78 | }, 79 | }) 80 | 81 | const opPath = "/resource" 82 | ctx = ctxutil.SetOpPath(ctx, opPath) 83 | 84 | respBody, resp, err := putRequest[schema.ActionGetResponse](ctx, client, opPath, map[string]string{"hello": "world"}) 85 | require.NoError(t, err) 86 | require.NotNil(t, resp) 87 | require.NotNil(t, respBody) 88 | 89 | require.Equal(t, int64(1234), respBody.Action.ID) 90 | }) 91 | 92 | t.Run("DELETE", func(t *testing.T) { 93 | ctx, server, client := makeTestUtils(t) 94 | 95 | server.Expect([]mockutil.Request{ 96 | { 97 | Method: "DELETE", Path: "/resource", 98 | Status: 200, 99 | JSON: schema.ActionGetResponse{Action: schema.Action{ID: 1234}}, 100 | }, 101 | }) 102 | 103 | const opPath = "/resource" 104 | ctx = ctxutil.SetOpPath(ctx, opPath) 105 | 106 | respBody, resp, err := deleteRequest[schema.ActionGetResponse](ctx, client, opPath) 107 | require.NoError(t, err) 108 | require.NotNil(t, resp) 109 | require.NotNil(t, respBody) 110 | 111 | require.Equal(t, int64(1234), respBody.Action.ID) 112 | }) 113 | 114 | t.Run("DELETE no result", func(t *testing.T) { 115 | ctx, server, client := makeTestUtils(t) 116 | 117 | server.Expect([]mockutil.Request{ 118 | { 119 | Method: "DELETE", Path: "/resource", 120 | Status: 204, 121 | }, 122 | }) 123 | 124 | const opPath = "/resource" 125 | ctx = ctxutil.SetOpPath(ctx, opPath) 126 | 127 | resp, err := deleteRequestNoResult(ctx, client, opPath) 128 | require.NoError(t, err) 129 | require.NotNil(t, resp) 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /hcloud/client_handler.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // handler is an interface representing a client request transaction. The handler are 9 | // meant to be chained, similarly to the [http.RoundTripper] interface. 10 | // 11 | // The handler chain is placed between the [Client] API operations and the 12 | // [http.Client]. 13 | type handler interface { 14 | Do(req *http.Request, v any) (resp *Response, err error) 15 | } 16 | 17 | // assembleHandlerChain assembles the chain of handlers used to make API requests. 18 | // 19 | // The order of the handlers is important. 20 | func assembleHandlerChain(client *Client) handler { 21 | // Start down the chain: sending the http request 22 | h := newHTTPHandler(client.httpClient) 23 | 24 | // Insert debug writer if enabled 25 | if client.debugWriter != nil { 26 | h = wrapDebugHandler(h, client.debugWriter) 27 | } 28 | 29 | // Read rate limit headers 30 | h = wrapRateLimitHandler(h) 31 | 32 | // Build error from response 33 | h = wrapErrorHandler(h) 34 | 35 | // Retry request if condition are met 36 | h = wrapRetryHandler(h, client.retryBackoffFunc, client.retryMaxRetries) 37 | 38 | // Finally parse the response body into the provided schema 39 | h = wrapParseHandler(h) 40 | 41 | return h 42 | } 43 | 44 | // cloneRequest clones both the request and the request body. 45 | func cloneRequest(req *http.Request, ctx context.Context) (cloned *http.Request, err error) { //revive:disable:context-as-argument 46 | cloned = req.Clone(ctx) 47 | 48 | if req.ContentLength > 0 { 49 | cloned.Body, err = req.GetBody() 50 | if err != nil { 51 | return nil, err 52 | } 53 | } 54 | 55 | return cloned, nil 56 | } 57 | -------------------------------------------------------------------------------- /hcloud/client_handler_debug.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httputil" 9 | ) 10 | 11 | func wrapDebugHandler(wrapped handler, output io.Writer) handler { 12 | return &debugHandler{wrapped, output} 13 | } 14 | 15 | type debugHandler struct { 16 | handler handler 17 | output io.Writer 18 | } 19 | 20 | func (h *debugHandler) Do(req *http.Request, v any) (resp *Response, err error) { 21 | // Clone the request, so we can redact the auth header, read the body 22 | // and use a new context. 23 | cloned, err := cloneRequest(req, context.Background()) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | cloned.Header.Set("Authorization", "REDACTED") 29 | 30 | dumpReq, err := httputil.DumpRequestOut(cloned, true) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | fmt.Fprintf(h.output, "--- Request:\n%s\n\n", dumpReq) 36 | 37 | resp, err = h.handler.Do(req, v) 38 | if err != nil { 39 | return resp, err 40 | } 41 | 42 | dumpResp, err := httputil.DumpResponse(resp.Response, true) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | fmt.Fprintf(h.output, "--- Response:\n%s\n\n", dumpResp) 48 | 49 | return resp, err 50 | } 51 | -------------------------------------------------------------------------------- /hcloud/client_handler_debug_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestDebugHandler(t *testing.T) { 16 | testCases := []struct { 17 | name string 18 | wrapped func(req *http.Request, v any) (*Response, error) 19 | want string 20 | }{ 21 | { 22 | name: "network error", 23 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 24 | return nil, fmt.Errorf("network error") 25 | }, 26 | want: `--- Request: 27 | GET /v1/ HTTP/1.1 28 | Host: api.hetzner.cloud 29 | User-Agent: hcloud-go/testing 30 | Authorization: REDACTED 31 | Accept-Encoding: gzip 32 | 33 | 34 | 35 | `, 36 | }, 37 | { 38 | name: "http 503 error", 39 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 40 | return fakeResponse(t, 503, "", false), nil 41 | }, 42 | want: `--- Request: 43 | GET /v1/ HTTP/1.1 44 | Host: api.hetzner.cloud 45 | User-Agent: hcloud-go/testing 46 | Authorization: REDACTED 47 | Accept-Encoding: gzip 48 | 49 | 50 | 51 | --- Response: 52 | HTTP/1.1 503 Service Unavailable 53 | Connection: close 54 | 55 | 56 | 57 | `, 58 | }, 59 | { 60 | name: "http 200", 61 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 62 | return fakeResponse(t, 200, `{"data": {"id": 1234, "name": "testing"}}`, true), nil 63 | }, 64 | want: `--- Request: 65 | GET /v1/ HTTP/1.1 66 | Host: api.hetzner.cloud 67 | User-Agent: hcloud-go/testing 68 | Authorization: REDACTED 69 | Accept-Encoding: gzip 70 | 71 | 72 | 73 | --- Response: 74 | HTTP/1.1 200 OK 75 | Connection: close 76 | Content-Type: application/json 77 | 78 | {"data": {"id": 1234, "name": "testing"}} 79 | 80 | `, 81 | }, 82 | } 83 | for _, testCase := range testCases { 84 | t.Run(testCase.name, func(t *testing.T) { 85 | buf := bytes.NewBuffer(nil) 86 | 87 | m := &mockHandler{testCase.wrapped} 88 | h := wrapDebugHandler(m, buf) 89 | 90 | client := NewClient(WithToken("dummy")) 91 | client.userAgent = "hcloud-go/testing" 92 | 93 | req, err := client.NewRequest(context.Background(), "GET", "/", nil) 94 | require.NoError(t, err) 95 | 96 | h.Do(req, nil) 97 | 98 | re := regexp.MustCompile(`\r`) 99 | output := re.ReplaceAllString(buf.String(), "") 100 | assert.Equal(t, testCase.want, output) 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /hcloud/client_handler_error.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 10 | ) 11 | 12 | var ErrStatusCode = errors.New("server responded with status code") 13 | 14 | func wrapErrorHandler(wrapped handler) handler { 15 | return &errorHandler{wrapped} 16 | } 17 | 18 | type errorHandler struct { 19 | handler handler 20 | } 21 | 22 | func (h *errorHandler) Do(req *http.Request, v any) (resp *Response, err error) { 23 | resp, err = h.handler.Do(req, v) 24 | if err != nil { 25 | return resp, err 26 | } 27 | 28 | if resp.StatusCode >= 400 && resp.StatusCode <= 599 { 29 | err = errorFromBody(resp) 30 | if err == nil { 31 | err = fmt.Errorf("hcloud: %w %d", ErrStatusCode, resp.StatusCode) 32 | } 33 | } 34 | return resp, err 35 | } 36 | 37 | func errorFromBody(resp *Response) error { 38 | if !resp.hasJSONBody() { 39 | return nil 40 | } 41 | 42 | var s schema.ErrorResponse 43 | if err := json.Unmarshal(resp.body, &s); err != nil { 44 | return nil // nolint: nilerr 45 | } 46 | if s.Error.Code == "" && s.Error.Message == "" { 47 | return nil 48 | } 49 | 50 | hcErr := ErrorFromSchema(s.Error) 51 | hcErr.response = resp 52 | return hcErr 53 | } 54 | -------------------------------------------------------------------------------- /hcloud/client_handler_error_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestErrorHandler(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | wrapped func(req *http.Request, v any) (*Response, error) 15 | want func(t *testing.T, resp *Response, err error) 16 | }{ 17 | { 18 | name: "no error", 19 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 20 | return fakeResponse(t, 200, `{"data": "Hello"}`, true), nil 21 | }, 22 | want: func(t *testing.T, resp *Response, err error) { 23 | assert.Equal(t, 200, resp.StatusCode) 24 | assert.NoError(t, err) 25 | }, 26 | }, 27 | { 28 | name: "network error", 29 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 30 | return nil, fmt.Errorf("network error") 31 | }, 32 | want: func(t *testing.T, resp *Response, err error) { 33 | assert.Nil(t, resp) 34 | assert.EqualError(t, err, "network error") 35 | }, 36 | }, 37 | { 38 | name: "http 503 error", 39 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 40 | return fakeResponse(t, 503, "", false), nil 41 | }, 42 | want: func(t *testing.T, resp *Response, err error) { 43 | assert.Equal(t, 503, resp.StatusCode) 44 | assert.EqualError(t, err, "hcloud: server responded with status code 503") 45 | }, 46 | }, 47 | { 48 | name: "http 422 error", 49 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 50 | return fakeResponse(t, 422, `{"error": {"code": "service_error", "message": "An error occurred"}}`, true), nil 51 | }, 52 | want: func(t *testing.T, resp *Response, err error) { 53 | assert.Equal(t, 422, resp.StatusCode) 54 | assert.EqualError(t, err, "An error occurred (service_error)") 55 | }, 56 | }, 57 | } 58 | for _, testCase := range testCases { 59 | t.Run(testCase.name, func(t *testing.T) { 60 | m := &mockHandler{testCase.wrapped} 61 | h := wrapErrorHandler(m) 62 | 63 | resp, err := h.Do(nil, nil) 64 | 65 | testCase.want(t, resp, err) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /hcloud/client_handler_http.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func newHTTPHandler(httpClient *http.Client) handler { 8 | return &httpHandler{httpClient} 9 | } 10 | 11 | type httpHandler struct { 12 | httpClient *http.Client 13 | } 14 | 15 | func (h *httpHandler) Do(req *http.Request, _ interface{}) (*Response, error) { 16 | httpResponse, err := h.httpClient.Do(req) //nolint: bodyclose 17 | resp := &Response{Response: httpResponse} 18 | if err != nil { 19 | return resp, err 20 | } 21 | 22 | err = resp.populateBody() 23 | if err != nil { 24 | return resp, err 25 | } 26 | 27 | return resp, err 28 | } 29 | -------------------------------------------------------------------------------- /hcloud/client_handler_http_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestHTTPHandler(t *testing.T) { 14 | testServer := httptest.NewServer( 15 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | w.Write([]byte("hello")) 17 | }), 18 | ) 19 | 20 | h := newHTTPHandler(&http.Client{}) 21 | 22 | req, err := http.NewRequest("GET", testServer.URL, nil) 23 | require.NoError(t, err) 24 | 25 | resp, err := h.Do(req, nil) 26 | require.NoError(t, err) 27 | 28 | // Ensure the internal response body is populated 29 | assert.Equal(t, []byte("hello"), resp.body) 30 | 31 | // Ensure the original response body is readable by external users 32 | body, err := io.ReadAll(resp.Body) 33 | resp.Body.Close() 34 | require.NoError(t, err) 35 | assert.Equal(t, []byte("hello"), body) 36 | } 37 | -------------------------------------------------------------------------------- /hcloud/client_handler_parse.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 11 | ) 12 | 13 | func wrapParseHandler(wrapped handler) handler { 14 | return &parseHandler{wrapped} 15 | } 16 | 17 | type parseHandler struct { 18 | handler handler 19 | } 20 | 21 | func (h *parseHandler) Do(req *http.Request, v any) (resp *Response, err error) { 22 | // respBody is not needed down the handler chain 23 | resp, err = h.handler.Do(req, nil) 24 | if err != nil { 25 | return resp, err 26 | } 27 | 28 | if resp.hasJSONBody() { 29 | // Parse the response meta 30 | var s schema.MetaResponse 31 | if err := json.Unmarshal(resp.body, &s); err != nil { 32 | return resp, fmt.Errorf("hcloud: error reading response meta data: %w", err) 33 | } 34 | if s.Meta.Pagination != nil { 35 | p := PaginationFromSchema(*s.Meta.Pagination) 36 | resp.Meta.Pagination = &p 37 | } 38 | } 39 | 40 | // Parse the response schema 41 | if v != nil { 42 | if w, ok := v.(io.Writer); ok { 43 | _, err = io.Copy(w, bytes.NewReader(resp.body)) 44 | } else { 45 | err = json.Unmarshal(resp.body, v) 46 | } 47 | } 48 | 49 | return resp, err 50 | } 51 | -------------------------------------------------------------------------------- /hcloud/client_handler_parse_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestParseHandler(t *testing.T) { 12 | type SomeStruct struct { 13 | Data string `json:"data"` 14 | } 15 | 16 | testCases := []struct { 17 | name string 18 | wrapped func(req *http.Request, v any) (*Response, error) 19 | want func(t *testing.T, v SomeStruct, resp *Response, err error) 20 | }{ 21 | { 22 | name: "no error", 23 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 24 | return fakeResponse(t, 200, `{"data": "Hello", "meta": {"pagination": {"page": 1}}}`, true), nil 25 | }, 26 | want: func(t *testing.T, v SomeStruct, resp *Response, err error) { 27 | assert.NoError(t, err) 28 | assert.Equal(t, "Hello", v.Data) 29 | assert.Equal(t, 1, resp.Meta.Pagination.Page) 30 | }, 31 | }, 32 | { 33 | name: "any error", 34 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 35 | return nil, fmt.Errorf("any error") 36 | }, 37 | want: func(t *testing.T, v SomeStruct, resp *Response, err error) { 38 | assert.EqualError(t, err, "any error") 39 | assert.Equal(t, "", v.Data) 40 | assert.Nil(t, resp) 41 | }, 42 | }, 43 | } 44 | for _, testCase := range testCases { 45 | t.Run(testCase.name, func(t *testing.T) { 46 | m := &mockHandler{testCase.wrapped} 47 | h := wrapParseHandler(m) 48 | 49 | s := SomeStruct{} 50 | 51 | resp, err := h.Do(nil, &s) 52 | 53 | testCase.want(t, s, resp, err) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /hcloud/client_handler_rate_limit.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | func wrapRateLimitHandler(wrapped handler) handler { 10 | return &rateLimitHandler{wrapped} 11 | } 12 | 13 | type rateLimitHandler struct { 14 | handler handler 15 | } 16 | 17 | func (h *rateLimitHandler) Do(req *http.Request, v any) (resp *Response, err error) { 18 | resp, err = h.handler.Do(req, v) 19 | 20 | // Ensure the embedded [*http.Response] is not nil, e.g. on canceled context 21 | if resp != nil && resp.Response != nil && resp.Response.Header != nil { 22 | if h := resp.Header.Get("RateLimit-Limit"); h != "" { 23 | resp.Meta.Ratelimit.Limit, _ = strconv.Atoi(h) 24 | } 25 | if h := resp.Header.Get("RateLimit-Remaining"); h != "" { 26 | resp.Meta.Ratelimit.Remaining, _ = strconv.Atoi(h) 27 | } 28 | if h := resp.Header.Get("RateLimit-Reset"); h != "" { 29 | if ts, err := strconv.ParseInt(h, 10, 64); err == nil { 30 | resp.Meta.Ratelimit.Reset = time.Unix(ts, 0) 31 | } 32 | } 33 | } 34 | 35 | return resp, err 36 | } 37 | -------------------------------------------------------------------------------- /hcloud/client_handler_rate_limit_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRateLimitHandler(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | wrapped func(req *http.Request, v any) (*Response, error) 16 | want func(t *testing.T, resp *Response, err error) 17 | }{ 18 | { 19 | name: "response", 20 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 21 | resp := fakeResponse(t, 200, "", false) 22 | resp.Header.Set("RateLimit-Limit", "1000") 23 | resp.Header.Set("RateLimit-Remaining", "999") 24 | resp.Header.Set("RateLimit-Reset", "1511954577") 25 | return resp, nil 26 | }, 27 | want: func(t *testing.T, resp *Response, err error) { 28 | assert.NoError(t, err) 29 | assert.Equal(t, 1000, resp.Meta.Ratelimit.Limit) 30 | assert.Equal(t, 999, resp.Meta.Ratelimit.Remaining) 31 | assert.Equal(t, time.Unix(1511954577, 0), resp.Meta.Ratelimit.Reset) 32 | }, 33 | }, 34 | { 35 | name: "any error", 36 | wrapped: func(_ *http.Request, _ any) (*Response, error) { 37 | return nil, fmt.Errorf("any error") 38 | }, 39 | want: func(t *testing.T, resp *Response, err error) { 40 | assert.EqualError(t, err, "any error") 41 | assert.Nil(t, resp) 42 | }, 43 | }, 44 | } 45 | for _, testCase := range testCases { 46 | t.Run(testCase.name, func(t *testing.T) { 47 | m := &mockHandler{testCase.wrapped} 48 | h := wrapRateLimitHandler(m) 49 | 50 | resp, err := h.Do(nil, nil) 51 | 52 | testCase.want(t, resp, err) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /hcloud/client_handler_retry.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | func wrapRetryHandler(wrapped handler, backoffFunc BackoffFunc, maxRetries int) handler { 11 | return &retryHandler{wrapped, backoffFunc, maxRetries} 12 | } 13 | 14 | type retryHandler struct { 15 | handler handler 16 | backoffFunc BackoffFunc 17 | maxRetries int 18 | } 19 | 20 | func (h *retryHandler) Do(req *http.Request, v any) (resp *Response, err error) { 21 | retries := 0 22 | ctx := req.Context() 23 | 24 | for { 25 | // Clone the request using the original context 26 | cloned, err := cloneRequest(req, ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | resp, err = h.handler.Do(cloned, v) 32 | if err != nil { 33 | // Beware the diversity of the errors: 34 | // - request preparation 35 | // - network connectivity 36 | // - http status code (see [errorHandler]) 37 | if ctx.Err() != nil { 38 | // early return if the context was canceled or timed out 39 | return resp, err 40 | } 41 | 42 | if retries < h.maxRetries && retryPolicy(resp, err) { 43 | select { 44 | case <-ctx.Done(): 45 | return resp, err 46 | case <-time.After(h.backoffFunc(retries)): 47 | retries++ 48 | continue 49 | } 50 | } 51 | } 52 | 53 | return resp, err 54 | } 55 | } 56 | 57 | func retryPolicy(resp *Response, err error) bool { 58 | if err != nil { 59 | var apiErr Error 60 | var netErr net.Error 61 | 62 | switch { 63 | case errors.As(err, &apiErr): 64 | switch apiErr.Code { //nolint:exhaustive 65 | case ErrorCodeConflict: 66 | return true 67 | case ErrorCodeRateLimitExceeded: 68 | return true 69 | } 70 | case errors.Is(err, ErrStatusCode): 71 | switch resp.Response.StatusCode { 72 | // 5xx errors 73 | case http.StatusBadGateway, http.StatusGatewayTimeout: 74 | return true 75 | } 76 | case errors.As(err, &netErr): 77 | if netErr.Timeout() { 78 | return true 79 | } 80 | } 81 | } 82 | 83 | return false 84 | } 85 | -------------------------------------------------------------------------------- /hcloud/client_handler_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type mockHandler struct { 16 | f func(req *http.Request, v any) (resp *Response, err error) 17 | } 18 | 19 | func (h *mockHandler) Do(req *http.Request, v interface{}) (*Response, error) { return h.f(req, v) } 20 | 21 | func fakeResponse(t *testing.T, statusCode int, body string, json bool) *Response { 22 | t.Helper() 23 | 24 | w := httptest.NewRecorder() 25 | if body != "" && json { 26 | w.Header().Set("Content-Type", "application/json") 27 | } 28 | w.WriteHeader(statusCode) 29 | 30 | if body != "" { 31 | _, err := w.Write([]byte(body)) 32 | require.NoError(t, err) 33 | } 34 | 35 | resp := &Response{Response: w.Result()} //nolint: bodyclose 36 | require.NoError(t, resp.populateBody()) 37 | 38 | return resp 39 | } 40 | 41 | func TestCloneRequest(t *testing.T) { 42 | ctx := context.Background() 43 | 44 | req, err := http.NewRequest("GET", "/", bytes.NewBufferString("Hello")) 45 | require.NoError(t, err) 46 | req.Header.Set("Authorization", "sensitive") 47 | 48 | cloned, err := cloneRequest(req, ctx) 49 | require.NoError(t, err) 50 | cloned.Header.Set("Authorization", "REDACTED") 51 | cloned.Body = io.NopCloser(bytes.NewBufferString("Changed")) 52 | 53 | // Check context 54 | assert.Equal(t, req.Context(), cloned.Context()) 55 | 56 | // Check headers 57 | assert.Equal(t, "sensitive", req.Header.Get("Authorization")) 58 | 59 | // Check body 60 | reqBody, err := io.ReadAll(req.Body) 61 | require.NoError(t, err) 62 | assert.Equal(t, "Hello", string(reqBody)) 63 | } 64 | -------------------------------------------------------------------------------- /hcloud/client_helper.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | ) 7 | 8 | // allFromSchemaFunc transform each item in the list using the FromSchema function, and 9 | // returns the result. 10 | func allFromSchemaFunc[T, V any](all []T, fn func(T) V) []V { 11 | result := make([]V, len(all)) 12 | for i, t := range all { 13 | result[i] = fn(t) 14 | } 15 | 16 | return result 17 | } 18 | 19 | // iterPages fetches each pages using the list function, and returns the result. 20 | func iterPages[T any](listFn func(int) ([]*T, *Response, error)) ([]*T, error) { 21 | page := 1 22 | result := []*T{} 23 | 24 | for { 25 | pageResult, resp, err := listFn(page) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | result = append(result, pageResult...) 31 | 32 | if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 { 33 | return result, nil 34 | } 35 | page = resp.Meta.Pagination.NextPage 36 | } 37 | } 38 | 39 | // firstBy fetches a list of items using the list function, and returns the first item 40 | // of the list if present otherwise nil. 41 | func firstBy[T any](listFn func() ([]*T, *Response, error)) (*T, *Response, error) { 42 | items, resp, err := listFn() 43 | if len(items) == 0 { 44 | return nil, resp, err 45 | } 46 | 47 | return items[0], resp, err 48 | } 49 | 50 | // firstByName is a wrapper around [firstBy], that checks if the provided name is not 51 | // empty. 52 | func firstByName[T any](name string, listFn func() ([]*T, *Response, error)) (*T, *Response, error) { 53 | if name == "" { 54 | return nil, nil, nil 55 | } 56 | 57 | return firstBy(listFn) 58 | } 59 | 60 | // getByIDOrName fetches the resource by ID when the identifier is an integer, otherwise 61 | // by Name. To support resources that have a integer as Name, an additional attempt is 62 | // made to fetch the resource by Name using the ID. 63 | // 64 | // Since API managed resources (locations, server types, ...) do not have integers as 65 | // names, this function is only meaningful for user managed resources (ssh keys, 66 | // servers). 67 | func getByIDOrName[T any]( 68 | ctx context.Context, 69 | getByIDFn func(ctx context.Context, id int64) (*T, *Response, error), 70 | getByNameFn func(ctx context.Context, name string) (*T, *Response, error), 71 | idOrName string, 72 | ) (*T, *Response, error) { 73 | if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil { 74 | result, resp, err := getByIDFn(ctx, id) 75 | if err != nil { 76 | return result, resp, err 77 | } 78 | if result != nil { 79 | return result, resp, err 80 | } 81 | // Fallback to get by Name if the resource was not found 82 | } 83 | 84 | return getByNameFn(ctx, idOrName) 85 | } 86 | -------------------------------------------------------------------------------- /hcloud/client_helper_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIterPages(t *testing.T) { 11 | t.Run("succeed", func(t *testing.T) { 12 | result, err := iterPages(func(page int) ([]*int, *Response, error) { 13 | if page < 4 { 14 | return []*int{Ptr(page)}, &Response{Meta: Meta{Pagination: &Pagination{NextPage: page + 1}}}, nil 15 | } 16 | return []*int{Ptr(page)}, &Response{}, nil 17 | }) 18 | require.NoError(t, err) 19 | require.Equal(t, []*int{Ptr(1), Ptr(2), Ptr(3), Ptr(4)}, result) 20 | }) 21 | 22 | t.Run("failed", func(t *testing.T) { 23 | result, err := iterPages(func(page int) ([]*int, *Response, error) { 24 | if page < 4 { 25 | return []*int{Ptr(page)}, &Response{Meta: Meta{Pagination: &Pagination{NextPage: page + 1}}}, nil 26 | } 27 | return nil, &Response{}, fmt.Errorf("failure") 28 | }) 29 | require.EqualError(t, err, "failure") 30 | require.Nil(t, result) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /hcloud/datacenter.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | 9 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 10 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 11 | ) 12 | 13 | // Datacenter represents a datacenter in the Hetzner Cloud. 14 | type Datacenter struct { 15 | ID int64 16 | Name string 17 | Description string 18 | Location *Location 19 | ServerTypes DatacenterServerTypes 20 | } 21 | 22 | // DatacenterServerTypes represents the server types available and supported in a datacenter. 23 | type DatacenterServerTypes struct { 24 | Supported []*ServerType 25 | AvailableForMigration []*ServerType 26 | Available []*ServerType 27 | } 28 | 29 | // DatacenterClient is a client for the datacenter API. 30 | type DatacenterClient struct { 31 | client *Client 32 | } 33 | 34 | // GetByID retrieves a datacenter by its ID. If the datacenter does not exist, nil is returned. 35 | func (c *DatacenterClient) GetByID(ctx context.Context, id int64) (*Datacenter, *Response, error) { 36 | const opPath = "/datacenters/%d" 37 | ctx = ctxutil.SetOpPath(ctx, opPath) 38 | 39 | reqPath := fmt.Sprintf(opPath, id) 40 | 41 | respBody, resp, err := getRequest[schema.DatacenterGetResponse](ctx, c.client, reqPath) 42 | if err != nil { 43 | if IsError(err, ErrorCodeNotFound) { 44 | return nil, resp, nil 45 | } 46 | return nil, resp, err 47 | } 48 | 49 | return DatacenterFromSchema(respBody.Datacenter), resp, nil 50 | } 51 | 52 | // GetByName retrieves a datacenter by its name. If the datacenter does not exist, nil is returned. 53 | func (c *DatacenterClient) GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) { 54 | return firstByName(name, func() ([]*Datacenter, *Response, error) { 55 | return c.List(ctx, DatacenterListOpts{Name: name}) 56 | }) 57 | } 58 | 59 | // Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it 60 | // retrieves a datacenter by its name. If the datacenter does not exist, nil is returned. 61 | func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) { 62 | if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil { 63 | return c.GetByID(ctx, id) 64 | } 65 | return c.GetByName(ctx, idOrName) 66 | } 67 | 68 | // DatacenterListOpts specifies options for listing datacenters. 69 | type DatacenterListOpts struct { 70 | ListOpts 71 | Name string 72 | Sort []string 73 | } 74 | 75 | func (l DatacenterListOpts) values() url.Values { 76 | vals := l.ListOpts.Values() 77 | if l.Name != "" { 78 | vals.Add("name", l.Name) 79 | } 80 | for _, sort := range l.Sort { 81 | vals.Add("sort", sort) 82 | } 83 | return vals 84 | } 85 | 86 | // List returns a list of datacenters for a specific page. 87 | // 88 | // Please note that filters specified in opts are not taken into account 89 | // when their value corresponds to their zero value or when they are empty. 90 | func (c *DatacenterClient) List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) { 91 | const opPath = "/datacenters?%s" 92 | ctx = ctxutil.SetOpPath(ctx, opPath) 93 | 94 | reqPath := fmt.Sprintf(opPath, opts.values().Encode()) 95 | 96 | respBody, resp, err := getRequest[schema.DatacenterListResponse](ctx, c.client, reqPath) 97 | if err != nil { 98 | return nil, resp, err 99 | } 100 | 101 | return allFromSchemaFunc(respBody.Datacenters, DatacenterFromSchema), resp, nil 102 | } 103 | 104 | // All returns all datacenters. 105 | func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) { 106 | return c.AllWithOpts(ctx, DatacenterListOpts{ListOpts: ListOpts{PerPage: 50}}) 107 | } 108 | 109 | // AllWithOpts returns all datacenters for the given options. 110 | func (c *DatacenterClient) AllWithOpts(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, error) { 111 | return iterPages(func(page int) ([]*Datacenter, *Response, error) { 112 | opts.Page = page 113 | return c.List(ctx, opts) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /hcloud/deprecation.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import "time" 4 | 5 | // Deprecatable is a shared interface implemented by all Resources that have a defined deprecation workflow. 6 | type Deprecatable interface { 7 | // IsDeprecated returns true if the resource is marked as deprecated. 8 | IsDeprecated() bool 9 | 10 | // UnavailableAfter returns the time that the deprecated resource will be removed from the API. 11 | // This only returns a valid value if [Deprecatable.IsDeprecated] returned true. 12 | UnavailableAfter() time.Time 13 | 14 | // DeprecationAnnounced returns the time that the deprecation of this resource was announced. 15 | // This only returns a valid value if [Deprecatable.IsDeprecated] returned true. 16 | DeprecationAnnounced() time.Time 17 | } 18 | 19 | // DeprecationInfo contains the information published when a resource is actually deprecated. 20 | type DeprecationInfo struct { 21 | Announced time.Time 22 | UnavailableAfter time.Time 23 | } 24 | 25 | // DeprecatableResource implements the [Deprecatable] interface and can be embedded in structs for Resources that can be 26 | // deprecated. 27 | type DeprecatableResource struct { 28 | Deprecation *DeprecationInfo 29 | } 30 | 31 | // IsDeprecated returns true if the resource is marked as deprecated. 32 | func (d DeprecatableResource) IsDeprecated() bool { 33 | return d.Deprecation != nil 34 | } 35 | 36 | // UnavailableAfter returns the time that the deprecated resource will be removed from the API. 37 | // This only returns a valid value if [Deprecatable.IsDeprecated] returned true. 38 | func (d DeprecatableResource) UnavailableAfter() time.Time { 39 | if !d.IsDeprecated() { 40 | // Return "null" time if resource is not deprecated 41 | return time.Unix(0, 0) 42 | } 43 | 44 | return d.Deprecation.UnavailableAfter 45 | } 46 | 47 | // DeprecationAnnounced returns the time that the deprecation of this resource was announced. 48 | // This only returns a valid value if [Deprecatable.IsDeprecated] returned true. 49 | func (d DeprecatableResource) DeprecationAnnounced() time.Time { 50 | if !d.IsDeprecated() { 51 | // Return "null" time if resource is not deprecated 52 | return time.Unix(0, 0) 53 | } 54 | 55 | return d.Deprecation.Announced 56 | } 57 | 58 | // Make sure that all expected Resources actually implement the interface. 59 | var _ Deprecatable = ServerType{} 60 | -------------------------------------------------------------------------------- /hcloud/deprecation_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type TestDeprecatableResource struct { 11 | DeprecatableResource 12 | } 13 | 14 | // Interface is implemented. 15 | var _ Deprecatable = TestDeprecatableResource{} 16 | 17 | func TestDeprecatableResource_IsDeprecated(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | resource TestDeprecatableResource 21 | want bool 22 | }{ 23 | {name: "nil returns false", resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: nil}}, want: false}, 24 | {name: "struct returns true", resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: &DeprecationInfo{}}}, want: true}, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | assert.Equalf(t, tt.want, tt.resource.IsDeprecated(), "IsDeprecated()") 29 | }) 30 | } 31 | } 32 | 33 | func TestDeprecatableResource_DeprecationAnnounced(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | resource TestDeprecatableResource 37 | want time.Time 38 | }{ 39 | { 40 | name: "nil returns default time", 41 | resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: nil}}, 42 | want: time.Unix(0, 0)}, 43 | { 44 | name: "actual value is returned", 45 | resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: &DeprecationInfo{Announced: mustParseTime(t, "2023-06-01T00:00:00+00:00")}}}, 46 | want: mustParseTime(t, "2023-06-01T00:00:00+00:00")}, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | assert.Equalf(t, tt.want, tt.resource.DeprecationAnnounced(), "DeprecationAnnounced()") 51 | }) 52 | } 53 | } 54 | 55 | func TestDeprecatableResource_UnavailableAfter(t *testing.T) { 56 | tests := []struct { 57 | name string 58 | resource TestDeprecatableResource 59 | want time.Time 60 | }{ 61 | { 62 | name: "nil returns default time", 63 | resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: nil}}, 64 | want: time.Unix(0, 0)}, 65 | { 66 | name: "actual value is returned", 67 | resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: &DeprecationInfo{UnavailableAfter: mustParseTime(t, "2023-06-01T00:00:00+00:00")}}}, 68 | want: mustParseTime(t, "2023-06-01T00:00:00+00:00")}, 69 | } 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | assert.Equalf(t, tt.want, tt.resource.UnavailableAfter(), "UnavailableAfter()") 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /hcloud/exp/README.md: -------------------------------------------------------------------------------- 1 | # Experimental 2 | 3 | The [`exp`](./) namespace holds experimental features for the `hcloud-go` library. The [`exp/kit`](./kit/) namespace is reserved for features not directly related to the `hcloud-go` library, for example the [`sshutil`](./kit/sshutil) package has functions to generate ssh keys. 4 | 5 | > [!CAUTION] 6 | > Breaking changes may occur without notice. Do not use in production! 7 | 8 | When an API reaches a certain level of stability, it may be moved out of the `exp` namespace. 9 | -------------------------------------------------------------------------------- /hcloud/exp/actionutil/actions.go: -------------------------------------------------------------------------------- 1 | package actionutil 2 | 3 | import "github.com/hetznercloud/hcloud-go/v2/hcloud" 4 | 5 | // AppendNext return the action and the next actions in a new slice. 6 | func AppendNext(action *hcloud.Action, nextActions []*hcloud.Action) []*hcloud.Action { 7 | all := make([]*hcloud.Action, 0, 1+len(nextActions)) 8 | all = append(all, action) 9 | all = append(all, nextActions...) 10 | return all 11 | } 12 | -------------------------------------------------------------------------------- /hcloud/exp/actionutil/actions_test.go: -------------------------------------------------------------------------------- 1 | package actionutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/hetznercloud/hcloud-go/v2/hcloud" 9 | ) 10 | 11 | func TestAppendNext(t *testing.T) { 12 | action := &hcloud.Action{ID: 1} 13 | nextActions := []*hcloud.Action{{ID: 2}, {ID: 3}} 14 | 15 | actions := AppendNext(action, nextActions) 16 | 17 | assert.Equal(t, []*hcloud.Action{{ID: 1}, {ID: 2}, {ID: 3}}, actions) 18 | } 19 | -------------------------------------------------------------------------------- /hcloud/exp/ctxutil/ctxutil.go: -------------------------------------------------------------------------------- 1 | package ctxutil 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | ) 7 | 8 | // key is an unexported type to prevents collisions with keys defined in other packages. 9 | type key struct{} 10 | 11 | // opPathKey is the key for operation path in Contexts. 12 | var opPathKey = key{} 13 | 14 | // SetOpPath processes the operation path and save it in the context before returning it. 15 | func SetOpPath(ctx context.Context, path string) context.Context { 16 | path, _, _ = strings.Cut(path, "?") 17 | path = strings.ReplaceAll(path, "%d", "-") 18 | path = strings.ReplaceAll(path, "%s", "-") 19 | 20 | return context.WithValue(ctx, opPathKey, path) 21 | } 22 | 23 | // OpPath returns the operation path from the context. 24 | func OpPath(ctx context.Context) string { 25 | result, ok := ctx.Value(opPathKey).(string) 26 | if !ok { 27 | return "" 28 | } 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /hcloud/exp/ctxutil/ctxutil_test.go: -------------------------------------------------------------------------------- 1 | package ctxutil 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestOpPath(t *testing.T) { 11 | for _, tt := range []struct { 12 | path string 13 | want string 14 | }{ 15 | { 16 | path: "/resource/%s/nested/%s/%d", 17 | want: "/resource/-/nested/-/-", 18 | }, 19 | { 20 | path: "/certificates/%d", 21 | want: "/certificates/-", 22 | }, 23 | { 24 | path: "/servers/%d/metrics?%s", 25 | want: "/servers/-/metrics", 26 | }, 27 | } { 28 | t.Run("", func(t *testing.T) { 29 | ctx := context.Background() 30 | 31 | require.Equal(t, tt.want, OpPath(SetOpPath(ctx, tt.path))) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hcloud/exp/doc.go: -------------------------------------------------------------------------------- 1 | // Package exp is a namespace that holds experimental features for the `hcloud-go` library. 2 | // 3 | // Breaking changes may occur without notice. Do not use in production! 4 | package exp 5 | -------------------------------------------------------------------------------- /hcloud/exp/kit/envutil/env.go: -------------------------------------------------------------------------------- 1 | package envutil 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | // LookupEnvWithFile retrieves the value of the environment variable named by the key (e.g. 10 | // HCLOUD_TOKEN). If the previous environment variable is not set, it retrieves the 11 | // content of the file located by a second environment variable named by the key + 12 | // '_FILE' (.e.g HCLOUD_TOKEN_FILE). 13 | // 14 | // For both cases, the returned value may be empty. 15 | // 16 | // The value from the environment takes precedence over the value from the file. 17 | func LookupEnvWithFile(key string) (string, error) { 18 | // Check if the value is set in the environment (e.g. HCLOUD_TOKEN) 19 | value, ok := os.LookupEnv(key) 20 | if ok { 21 | return value, nil 22 | } 23 | 24 | key += "_FILE" 25 | 26 | // Check if the value is set via a file (e.g. HCLOUD_TOKEN_FILE) 27 | valueFile, ok := os.LookupEnv(key) 28 | if !ok { 29 | // Validation of the value happens outside of this function 30 | return "", nil 31 | } 32 | 33 | // Read the content of the file 34 | valueBytes, err := os.ReadFile(valueFile) 35 | if err != nil { 36 | return "", fmt.Errorf("failed to read %s: %w", key, err) 37 | } 38 | 39 | return strings.TrimSpace(string(valueBytes)), nil 40 | } 41 | -------------------------------------------------------------------------------- /hcloud/exp/kit/envutil/env_test.go: -------------------------------------------------------------------------------- 1 | package envutil 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // nolint:unparam 13 | func writeTmpFile(t *testing.T, tmpDir, filename, content string) string { 14 | filepath := path.Join(tmpDir, filename) 15 | 16 | err := os.WriteFile(filepath, []byte(content), 0644) 17 | require.NoError(t, err) 18 | 19 | return filepath 20 | } 21 | 22 | func TestLookupEnvWithFile(t *testing.T) { 23 | testCases := []struct { 24 | name string 25 | setup func(t *testing.T, tmpDir string) 26 | want func(t *testing.T, value string, err error) 27 | }{ 28 | { 29 | name: "without any environment", 30 | setup: func(_ *testing.T, _ string) {}, 31 | want: func(t *testing.T, value string, err error) { 32 | assert.NoError(t, err) 33 | assert.Equal(t, "", value) 34 | }, 35 | }, 36 | { 37 | name: "value from environment", 38 | setup: func(t *testing.T, tmpDir string) { 39 | t.Setenv("CONFIG", "value") 40 | 41 | // Test for precedence 42 | filepath := writeTmpFile(t, tmpDir, "config", "content") 43 | t.Setenv("CONFIG_FILE", filepath) 44 | }, 45 | want: func(t *testing.T, value string, err error) { 46 | assert.NoError(t, err) 47 | assert.Equal(t, "value", value) 48 | }, 49 | }, 50 | { 51 | name: "empty value from environment", 52 | setup: func(t *testing.T, tmpDir string) { 53 | t.Setenv("CONFIG", "") 54 | 55 | // Test for precedence 56 | filepath := writeTmpFile(t, tmpDir, "config", "content") 57 | t.Setenv("CONFIG_FILE", filepath) 58 | }, 59 | want: func(t *testing.T, value string, err error) { 60 | assert.NoError(t, err) 61 | assert.Equal(t, "", value) 62 | }, 63 | }, 64 | { 65 | name: "value from file", 66 | setup: func(t *testing.T, tmpDir string) { 67 | // The extra spaces ensure that the value is sanitized 68 | filepath := writeTmpFile(t, tmpDir, "config", "content ") 69 | t.Setenv("CONFIG_FILE", filepath) 70 | }, 71 | want: func(t *testing.T, value string, err error) { 72 | assert.NoError(t, err) 73 | assert.Equal(t, "content", value) 74 | }, 75 | }, 76 | { 77 | name: "empty value from file", 78 | setup: func(t *testing.T, tmpDir string) { 79 | filepath := writeTmpFile(t, tmpDir, "config", "") 80 | t.Setenv("CONFIG_FILE", filepath) 81 | }, 82 | want: func(t *testing.T, value string, err error) { 83 | assert.NoError(t, err) 84 | assert.Equal(t, "", value) 85 | }, 86 | }, 87 | { 88 | name: "missing file", 89 | setup: func(t *testing.T, _ string) { 90 | t.Setenv("CONFIG_FILE", "/tmp/this-file-does-not-exits") 91 | }, 92 | want: func(t *testing.T, value string, err error) { 93 | assert.Error(t, err, "failed to read CONFIG_FILE: open /tmp/this-file-does-not-exits: no such file or directory") 94 | assert.Equal(t, "", value) 95 | }, 96 | }, 97 | } 98 | for _, testCase := range testCases { 99 | t.Run(testCase.name, func(t *testing.T) { 100 | tmpDir := t.TempDir() 101 | testCase.setup(t, tmpDir) 102 | value, err := LookupEnvWithFile("CONFIG") 103 | testCase.want(t, value, err) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /hcloud/exp/kit/randutil/id.go: -------------------------------------------------------------------------------- 1 | package randutil 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | ) 8 | 9 | // GenerateID returns a hex encoded random string with a len of 8 chars similar to 10 | // "2873fce7". 11 | func GenerateID() string { 12 | b := make([]byte, 4) 13 | _, err := rand.Read(b) 14 | if err != nil { 15 | // Should never happen as of go1.24: https://github.com/golang/go/issues/66821 16 | panic(fmt.Errorf("failed to generate random string: %w", err)) 17 | } 18 | return hex.EncodeToString(b) 19 | } 20 | -------------------------------------------------------------------------------- /hcloud/exp/kit/randutil/id_test.go: -------------------------------------------------------------------------------- 1 | package randutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGenerateRandomID(t *testing.T) { 10 | found1 := GenerateID() 11 | found2 := GenerateID() 12 | 13 | assert.Len(t, found1, 8) 14 | assert.Len(t, found2, 8) 15 | assert.NotEqual(t, found1, found2) 16 | } 17 | -------------------------------------------------------------------------------- /hcloud/exp/kit/sshutil/ssh_key.go: -------------------------------------------------------------------------------- 1 | package sshutil 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ed25519" 6 | "encoding/pem" 7 | "fmt" 8 | 9 | "golang.org/x/crypto/ssh" 10 | ) 11 | 12 | // GenerateKeyPair generates a new ed25519 ssh key pair, and returns the private key and 13 | // the public key respectively. 14 | func GenerateKeyPair() ([]byte, []byte, error) { 15 | pub, priv, err := ed25519.GenerateKey(nil) 16 | if err != nil { 17 | return nil, nil, fmt.Errorf("could not generate key pair: %w", err) 18 | } 19 | 20 | privBytes, err := encodePrivateKey(priv) 21 | if err != nil { 22 | return nil, nil, fmt.Errorf("could not encode private key: %w", err) 23 | } 24 | 25 | pubBytes, err := encodePublicKey(pub) 26 | if err != nil { 27 | return nil, nil, fmt.Errorf("could not encode public key: %w", err) 28 | } 29 | 30 | return privBytes, pubBytes, nil 31 | } 32 | 33 | func encodePrivateKey(priv crypto.PrivateKey) ([]byte, error) { 34 | privPem, err := ssh.MarshalPrivateKey(priv, "") 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return pem.EncodeToMemory(privPem), nil 40 | } 41 | 42 | func encodePublicKey(pub crypto.PublicKey) ([]byte, error) { 43 | sshPub, err := ssh.NewPublicKey(pub) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return ssh.MarshalAuthorizedKey(sshPub), nil 49 | } 50 | 51 | type privateKeyWithPublicKey interface { 52 | crypto.PrivateKey 53 | Public() crypto.PublicKey 54 | } 55 | 56 | // GeneratePublicKey generate a public key from the provided private key. 57 | func GeneratePublicKey(privBytes []byte) ([]byte, error) { 58 | priv, err := ssh.ParseRawPrivateKey(privBytes) 59 | if err != nil { 60 | return nil, fmt.Errorf("could not decode private key: %w", err) 61 | } 62 | 63 | key, ok := priv.(privateKeyWithPublicKey) 64 | if !ok { 65 | return nil, fmt.Errorf("private key doesn't export Public() crypto.PublicKey") 66 | } 67 | 68 | pubBytes, err := encodePublicKey(key.Public()) 69 | if err != nil { 70 | return nil, fmt.Errorf("could not encode public key: %w", err) 71 | } 72 | 73 | return pubBytes, nil 74 | } 75 | 76 | // GetPublicKeyFingerprint generate the finger print for the provided public key. 77 | func GetPublicKeyFingerprint(pubBytes []byte) (string, error) { 78 | pub, _, _, _, err := ssh.ParseAuthorizedKey(pubBytes) 79 | if err != nil { 80 | return "", fmt.Errorf("could not decode public key: %w", err) 81 | } 82 | 83 | fingerprint := ssh.FingerprintLegacyMD5(pub) 84 | 85 | return fingerprint, nil 86 | } 87 | -------------------------------------------------------------------------------- /hcloud/exp/kit/sshutil/ssh_key_test.go: -------------------------------------------------------------------------------- 1 | package sshutil 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGenerateKeyPair(t *testing.T) { 12 | privBytes, pubBytes, err := GenerateKeyPair() 13 | assert.NoError(t, err) 14 | 15 | priv := string(privBytes) 16 | pub := string(pubBytes) 17 | 18 | if !(strings.HasPrefix(priv, "-----BEGIN OPENSSH PRIVATE KEY-----\n") && 19 | strings.HasSuffix(priv, "-----END OPENSSH PRIVATE KEY-----\n")) { 20 | assert.Fail(t, "private key is invalid", priv) 21 | } 22 | 23 | if !strings.HasPrefix(pub, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA") { 24 | assert.Fail(t, "public key is invalid", pub) 25 | } 26 | } 27 | 28 | func TestGeneratePublicKey(t *testing.T) { 29 | privBytes, pubBytesOrig, err := GenerateKeyPair() 30 | require.NoError(t, err) 31 | 32 | pubBytes, err := GeneratePublicKey(privBytes) 33 | require.NoError(t, err) 34 | 35 | pub := string(pubBytes) 36 | priv := string(privBytes) 37 | 38 | if !(strings.HasPrefix(priv, "-----BEGIN OPENSSH PRIVATE KEY-----\n") && 39 | strings.HasSuffix(priv, "-----END OPENSSH PRIVATE KEY-----\n")) { 40 | assert.Fail(t, "private key is invalid", priv) 41 | } 42 | 43 | if !strings.HasPrefix(pub, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA") { 44 | assert.Fail(t, "public key is invalid", pub) 45 | } 46 | 47 | assert.Equal(t, pubBytesOrig, pubBytes) 48 | } 49 | 50 | func TestGetPublicKeyFingerprint(t *testing.T) { 51 | fingerprint, err := GetPublicKeyFingerprint([]byte(`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIccHCW76xx2rrPAUrjnuT6IjpEF1O+/U4IByVgv99Oi`)) 52 | require.NoError(t, err) 53 | assert.Equal(t, "77:79:69:b1:4d:c6:b6:45:6a:e9:52:29:04:3e:59:48", fingerprint) 54 | } 55 | -------------------------------------------------------------------------------- /hcloud/exp/labelutil/selector.go: -------------------------------------------------------------------------------- 1 | package labelutil 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // Selector combines the label set into a [label selector](https://docs.hetzner.cloud/#label-selector) that only selects 10 | // resources have all specified labels set. 11 | // 12 | // The selector string can be used to filter resources when listing, for example with [hcloud.ServerClient.AllWithOpts()]. 13 | func Selector(labels map[string]string) string { 14 | selectors := make([]string, 0, len(labels)) 15 | 16 | for k, v := range labels { 17 | selectors = append(selectors, fmt.Sprintf("%s=%s", k, v)) 18 | } 19 | 20 | // Reproducible result for tests 21 | sort.Strings(selectors) 22 | 23 | return strings.Join(selectors, ",") 24 | } 25 | -------------------------------------------------------------------------------- /hcloud/exp/labelutil/selector_test.go: -------------------------------------------------------------------------------- 1 | package labelutil 2 | 3 | import "testing" 4 | 5 | func TestSelector(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | labels map[string]string 9 | expectedSelector string 10 | }{ 11 | { 12 | name: "empty selector", 13 | labels: map[string]string{}, 14 | expectedSelector: "", 15 | }, 16 | { 17 | name: "single label", 18 | labels: map[string]string{"foo": "bar"}, 19 | expectedSelector: "foo=bar", 20 | }, 21 | { 22 | name: "multiple labels", 23 | labels: map[string]string{"foo": "bar", "foz": "baz"}, 24 | expectedSelector: "foo=bar,foz=baz", 25 | }, 26 | { 27 | name: "nil map", 28 | labels: nil, 29 | expectedSelector: "", 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if got := Selector(tt.labels); got != tt.expectedSelector { 35 | t.Errorf("Selector() = %v, want %v", got, tt.expectedSelector) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /hcloud/exp/mockutil/http.go: -------------------------------------------------------------------------------- 1 | package mockutil 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // Request describes a http request that a [httptest.Server] should receive, and the 14 | // corresponding response to return. 15 | // 16 | // Additional checks on the request (e.g. request body) may be added using the 17 | // [Request.Want] function. 18 | // 19 | // The response body is populated from either a JSON struct, or a JSON string. 20 | type Request struct { 21 | Method string 22 | Path string 23 | Want func(t *testing.T, r *http.Request) 24 | 25 | Status int 26 | JSON any 27 | JSONRaw string 28 | } 29 | 30 | // Handler is using a [Server] to mock http requests provided by the user. 31 | func Handler(t *testing.T, requests []Request) http.HandlerFunc { 32 | t.Helper() 33 | 34 | server := NewServer(t, requests) 35 | t.Cleanup(server.close) 36 | 37 | return server.handler 38 | } 39 | 40 | // NewServer returns a new mock server that closes itself at the end of the test. 41 | func NewServer(t *testing.T, requests []Request) *Server { 42 | t.Helper() 43 | 44 | o := &Server{t: t} 45 | o.Server = httptest.NewServer(http.HandlerFunc(o.handler)) 46 | t.Cleanup(o.close) 47 | 48 | o.Expect(requests) 49 | 50 | return o 51 | } 52 | 53 | // Server embeds a [httptest.Server] that answers HTTP calls with a list of expected [Request]. 54 | // 55 | // Request matching is based on the request count, and the user provided request will be 56 | // iterated over. 57 | // 58 | // A Server must be created using the [NewServer] function. 59 | type Server struct { 60 | *httptest.Server 61 | 62 | t *testing.T 63 | 64 | requests []Request 65 | index int 66 | } 67 | 68 | // Expect adds requests to the list of requests expected by the [Server]. 69 | func (m *Server) Expect(requests []Request) { 70 | m.requests = append(m.requests, requests...) 71 | } 72 | 73 | func (m *Server) close() { 74 | m.t.Helper() 75 | 76 | m.Server.Close() 77 | 78 | assert.EqualValues(m.t, len(m.requests), m.index, "expected more calls") 79 | } 80 | 81 | func (m *Server) handler(w http.ResponseWriter, r *http.Request) { 82 | if testing.Verbose() { 83 | m.t.Logf("call %d: %s %s\n", m.index, r.Method, r.RequestURI) 84 | } 85 | 86 | if m.index >= len(m.requests) { 87 | m.t.Fatalf("received unknown call %d", m.index) 88 | } 89 | 90 | expected := m.requests[m.index] 91 | 92 | expectedCall := expected.Method 93 | foundCall := r.Method 94 | if expected.Path != "" { 95 | expectedCall += " " + expected.Path 96 | foundCall += " " + r.RequestURI 97 | } 98 | require.Equal(m.t, expectedCall, foundCall) // nolint: testifylint 99 | 100 | if expected.Want != nil { 101 | expected.Want(m.t, r) 102 | } 103 | 104 | switch { 105 | case expected.JSON != nil: 106 | w.Header().Set("Content-Type", "application/json") 107 | w.WriteHeader(expected.Status) 108 | if err := json.NewEncoder(w).Encode(expected.JSON); err != nil { 109 | m.t.Fatal(err) 110 | } 111 | case expected.JSONRaw != "": 112 | w.Header().Set("Content-Type", "application/json") 113 | w.WriteHeader(expected.Status) 114 | _, err := w.Write([]byte(expected.JSONRaw)) 115 | if err != nil { 116 | m.t.Fatal(err) 117 | } 118 | default: 119 | w.WriteHeader(expected.Status) 120 | } 121 | 122 | m.index++ 123 | } 124 | -------------------------------------------------------------------------------- /hcloud/exp/mockutil/http_test.go: -------------------------------------------------------------------------------- 1 | package mockutil 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestHandler(t *testing.T) { 16 | server := NewServer(t, []Request{ 17 | { 18 | Method: "GET", Path: "/", 19 | Status: 200, 20 | JSON: struct { 21 | Data string `json:"data"` 22 | }{ 23 | Data: "Hello", 24 | }, 25 | }, 26 | { 27 | Method: "GET", Path: "/", 28 | Status: 400, 29 | JSONRaw: `{"error": "failed"}`, 30 | }, 31 | { 32 | Method: "GET", Path: "/", 33 | Status: 503, 34 | }, 35 | { 36 | Method: "GET", 37 | Want: func(t *testing.T, r *http.Request) { 38 | require.True(t, strings.HasPrefix(r.RequestURI, "/random?key=")) 39 | }, 40 | Status: 200, 41 | }, 42 | }) 43 | 44 | // Request 1 45 | resp, err := http.Get(server.URL) 46 | require.NoError(t, err) 47 | assert.Equal(t, 200, resp.StatusCode) 48 | assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) 49 | assert.JSONEq(t, `{"data":"Hello"}`, readBody(t, resp)) 50 | 51 | // Request 2 52 | resp, err = http.Get(server.URL) 53 | require.NoError(t, err) 54 | assert.Equal(t, 400, resp.StatusCode) 55 | assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) 56 | assert.JSONEq(t, `{"error": "failed"}`, readBody(t, resp)) 57 | 58 | // Request 3 59 | resp, err = http.Get(server.URL) 60 | require.NoError(t, err) 61 | assert.Equal(t, 503, resp.StatusCode) 62 | assert.Equal(t, "", resp.Header.Get("Content-Type")) 63 | assert.Equal(t, "", readBody(t, resp)) 64 | 65 | // Request 4 66 | resp, err = http.Get(fmt.Sprintf("%s/random?key=%d", server.URL, rand.Int63())) 67 | require.NoError(t, err) 68 | assert.Equal(t, 200, resp.StatusCode) 69 | assert.Equal(t, "", resp.Header.Get("Content-Type")) 70 | assert.Equal(t, "", readBody(t, resp)) 71 | 72 | // Extra request 5 73 | server.Expect([]Request{ 74 | {Method: "GET", Path: "/", Status: 200}, 75 | }) 76 | 77 | resp, err = http.Get(server.URL) 78 | require.NoError(t, err) 79 | assert.Equal(t, 200, resp.StatusCode) 80 | assert.Equal(t, "", resp.Header.Get("Content-Type")) 81 | assert.Equal(t, "", readBody(t, resp)) 82 | } 83 | 84 | func readBody(t *testing.T, resp *http.Response) string { 85 | t.Helper() 86 | 87 | body, err := io.ReadAll(resp.Body) 88 | require.NoError(t, err) 89 | require.NoError(t, resp.Body.Close()) 90 | return strings.TrimSuffix(string(body), "\n") 91 | } 92 | -------------------------------------------------------------------------------- /hcloud/hcloud.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package hcloud is a library for the Hetzner Cloud API. 3 | 4 | The Hetzner Cloud API reference is available at https://docs.hetzner.cloud. 5 | 6 | Make sure to follow our API changelog available at https://docs.hetzner.cloud/changelog 7 | (or the RRS feed available at https://docs.hetzner.cloud/changelog/feed.rss) to be 8 | notified about additions, deprecations and removals. 9 | 10 | # Retry mechanism 11 | 12 | The [Client.Do] method will retry failed requests that match certain criteria. The 13 | default retry interval is defined by an exponential backoff algorithm truncated to 60s 14 | with jitter. The default maximal number of retries is 5. 15 | 16 | The following rules defines when a request can be retried: 17 | 18 | When the [http.Client] returned a network timeout error. 19 | 20 | When the API returned an HTTP error, with the status code: 21 | - [http.StatusBadGateway] 22 | - [http.StatusGatewayTimeout] 23 | 24 | When the API returned an application error, with the code: 25 | - [ErrorCodeConflict] 26 | - [ErrorCodeRateLimitExceeded] 27 | 28 | Changes to the retry policy might occur between releases, and will not be considered 29 | breaking changes. 30 | */ 31 | package hcloud 32 | 33 | // Version is the library's version following Semantic Versioning. 34 | const Version = "2.21.1" // x-releaser-pleaser-version 35 | -------------------------------------------------------------------------------- /hcloud/hcloud_test.go: -------------------------------------------------------------------------------- 1 | package hcloud_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/hetznercloud/hcloud-go/v2/hcloud" 9 | ) 10 | 11 | func Example() { 12 | client := hcloud.NewClient(hcloud.WithToken("token")) 13 | 14 | server, _, err := client.Server.GetByID(context.Background(), 1) 15 | if err != nil { 16 | log.Fatalf("error retrieving server: %s\n", err) 17 | } 18 | if server != nil { 19 | fmt.Printf("server 1 is called %q\n", server.Name) 20 | } else { 21 | fmt.Println("server 1 not found") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /hcloud/helper.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import "time" 4 | 5 | // Ptr returns a pointer to p. 6 | func Ptr[T any](p T) *T { 7 | return &p 8 | } 9 | 10 | // String returns a pointer to the passed string s. 11 | // 12 | // Deprecated: Use [Ptr] instead. 13 | func String(s string) *string { return Ptr(s) } 14 | 15 | // Int returns a pointer to the passed integer i. 16 | // 17 | // Deprecated: Use [Ptr] instead. 18 | func Int(i int) *int { return Ptr(i) } 19 | 20 | // Bool returns a pointer to the passed bool b. 21 | // 22 | // Deprecated: Use [Ptr] instead. 23 | func Bool(b bool) *bool { return Ptr(b) } 24 | 25 | // Duration returns a pointer to the passed time.Duration d. 26 | // 27 | // Deprecated: Use [Ptr] instead. 28 | func Duration(d time.Duration) *time.Duration { return Ptr(d) } 29 | -------------------------------------------------------------------------------- /hcloud/interface_gen.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | //go:generate go run github.com/vburenin/ifacemaker -f action.go -f action_watch.go -f action_waiter.go -s ActionClient -i IActionClient -p hcloud -o zz_action_client_iface.go 4 | //go:generate go run github.com/vburenin/ifacemaker -f action.go -s ResourceActionClient -i IResourceActionClient -p hcloud -o zz_resource_action_client_iface.go 5 | //go:generate go run github.com/vburenin/ifacemaker -f datacenter.go -s DatacenterClient -i IDatacenterClient -p hcloud -o zz_datacenter_client_iface.go 6 | //go:generate go run github.com/vburenin/ifacemaker -f floating_ip.go -s FloatingIPClient -i IFloatingIPClient -p hcloud -o zz_floating_ip_client_iface.go 7 | //go:generate go run github.com/vburenin/ifacemaker -f image.go -s ImageClient -i IImageClient -p hcloud -o zz_image_client_iface.go 8 | //go:generate go run github.com/vburenin/ifacemaker -f iso.go -s ISOClient -i IISOClient -p hcloud -o zz_iso_client_iface.go 9 | //go:generate go run github.com/vburenin/ifacemaker -f location.go -s LocationClient -i ILocationClient -p hcloud -o zz_location_client_iface.go 10 | //go:generate go run github.com/vburenin/ifacemaker -f network.go -s NetworkClient -i INetworkClient -p hcloud -o zz_network_client_iface.go 11 | //go:generate go run github.com/vburenin/ifacemaker -f pricing.go -s PricingClient -i IPricingClient -p hcloud -o zz_pricing_client_iface.go 12 | //go:generate go run github.com/vburenin/ifacemaker -f server.go -s ServerClient -i IServerClient -p hcloud -o zz_server_client_iface.go 13 | //go:generate go run github.com/vburenin/ifacemaker -f server_type.go -s ServerTypeClient -i IServerTypeClient -p hcloud -o zz_server_type_client_iface.go 14 | //go:generate go run github.com/vburenin/ifacemaker -f ssh_key.go -s SSHKeyClient -i ISSHKeyClient -p hcloud -o zz_ssh_key_client_iface.go 15 | //go:generate go run github.com/vburenin/ifacemaker -f volume.go -s VolumeClient -i IVolumeClient -p hcloud -o zz_volume_client_iface.go 16 | //go:generate go run github.com/vburenin/ifacemaker -f load_balancer.go -s LoadBalancerClient -i ILoadBalancerClient -p hcloud -o zz_load_balancer_client_iface.go 17 | //go:generate go run github.com/vburenin/ifacemaker -f load_balancer_type.go -s LoadBalancerTypeClient -i ILoadBalancerTypeClient -p hcloud -o zz_load_balancer_type_client_iface.go 18 | //go:generate go run github.com/vburenin/ifacemaker -f certificate.go -s CertificateClient -i ICertificateClient -p hcloud -o zz_certificate_client_iface.go 19 | //go:generate go run github.com/vburenin/ifacemaker -f firewall.go -s FirewallClient -i IFirewallClient -p hcloud -o zz_firewall_client_iface.go 20 | //go:generate go run github.com/vburenin/ifacemaker -f placement_group.go -s PlacementGroupClient -i IPlacementGroupClient -p hcloud -o zz_placement_group_client_iface.go 21 | //go:generate go run github.com/vburenin/ifacemaker -f rdns.go -s RDNSClient -i IRDNSClient -p hcloud -o zz_rdns_client_iface.go 22 | //go:generate go run github.com/vburenin/ifacemaker -f primary_ip.go -s PrimaryIPClient -i IPrimaryIPClient -p hcloud -o zz_primary_ip_client_iface.go 23 | -------------------------------------------------------------------------------- /hcloud/internal/instrumentation/metrics_test.go: -------------------------------------------------------------------------------- 1 | package instrumentation 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMultipleInstrumentedClients(t *testing.T) { 12 | reg := prometheus.NewRegistry() 13 | 14 | t.Run("should not panic", func(_ *testing.T) { 15 | // Following code should run without panicking 16 | New("test", reg).InstrumentedRoundTripper(http.DefaultTransport) 17 | New("test", reg).InstrumentedRoundTripper(http.DefaultTransport) 18 | }) 19 | } 20 | 21 | func TestPreparePathForLabel(t *testing.T) { 22 | tests := []struct { 23 | path string 24 | want string 25 | }{ 26 | { 27 | "/v1/volumes/123456", 28 | "/volumes/-", 29 | }, 30 | { 31 | "/v1/volumes/123456/actions/attach", 32 | "/volumes/-/actions/attach", 33 | }, 34 | } 35 | for _, tt := range tests { 36 | t.Run("", func(t *testing.T) { 37 | assert.Equal(t, tt.want, preparePathForLabel(tt.path)) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /hcloud/iso.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 10 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 11 | ) 12 | 13 | // ISO represents an ISO image in the Hetzner Cloud. 14 | type ISO struct { 15 | ID int64 16 | Name string 17 | Description string 18 | Type ISOType 19 | Architecture *Architecture 20 | // Deprecated: Use [ISO.Deprecation] instead. 21 | Deprecated time.Time 22 | DeprecatableResource 23 | } 24 | 25 | // ISOType specifies the type of an ISO image. 26 | type ISOType string 27 | 28 | const ( 29 | // ISOTypePublic is the type of a public ISO image. 30 | ISOTypePublic ISOType = "public" 31 | 32 | // ISOTypePrivate is the type of a private ISO image. 33 | ISOTypePrivate ISOType = "private" 34 | ) 35 | 36 | // ISOClient is a client for the ISO API. 37 | type ISOClient struct { 38 | client *Client 39 | } 40 | 41 | // GetByID retrieves an ISO by its ID. 42 | func (c *ISOClient) GetByID(ctx context.Context, id int64) (*ISO, *Response, error) { 43 | const opPath = "/isos/%d" 44 | ctx = ctxutil.SetOpPath(ctx, opPath) 45 | 46 | reqPath := fmt.Sprintf(opPath, id) 47 | 48 | respBody, resp, err := getRequest[schema.ISOGetResponse](ctx, c.client, reqPath) 49 | if err != nil { 50 | if IsError(err, ErrorCodeNotFound) { 51 | return nil, resp, nil 52 | } 53 | return nil, resp, err 54 | } 55 | 56 | return ISOFromSchema(respBody.ISO), resp, nil 57 | } 58 | 59 | // GetByName retrieves an ISO by its name. 60 | func (c *ISOClient) GetByName(ctx context.Context, name string) (*ISO, *Response, error) { 61 | return firstByName(name, func() ([]*ISO, *Response, error) { 62 | return c.List(ctx, ISOListOpts{Name: name}) 63 | }) 64 | } 65 | 66 | // Get retrieves an ISO by its ID if the input can be parsed as an integer, otherwise it retrieves an ISO by its name. 67 | func (c *ISOClient) Get(ctx context.Context, idOrName string) (*ISO, *Response, error) { 68 | return getByIDOrName(ctx, c.GetByID, c.GetByName, idOrName) 69 | } 70 | 71 | // ISOListOpts specifies options for listing isos. 72 | type ISOListOpts struct { 73 | ListOpts 74 | Name string 75 | Sort []string 76 | // Architecture filters the ISOs by Architecture. Note that custom ISOs do not have any architecture set, and you 77 | // must use IncludeWildcardArchitecture to include them. 78 | Architecture []Architecture 79 | // IncludeWildcardArchitecture must be set to also return custom ISOs that have no architecture set, if you are 80 | // also setting the Architecture field. 81 | // Deprecated: Use [ISOListOpts.IncludeArchitectureWildcard] instead. 82 | IncludeWildcardArchitecture bool 83 | // IncludeWildcardArchitecture must be set to also return custom ISOs that have no architecture set, if you are 84 | // also setting the Architecture field. 85 | IncludeArchitectureWildcard bool 86 | } 87 | 88 | func (l ISOListOpts) values() url.Values { 89 | vals := l.ListOpts.Values() 90 | if l.Name != "" { 91 | vals.Add("name", l.Name) 92 | } 93 | for _, sort := range l.Sort { 94 | vals.Add("sort", sort) 95 | } 96 | for _, arch := range l.Architecture { 97 | vals.Add("architecture", string(arch)) 98 | } 99 | if l.IncludeArchitectureWildcard || l.IncludeWildcardArchitecture { 100 | vals.Add("include_architecture_wildcard", "true") 101 | } 102 | return vals 103 | } 104 | 105 | // List returns a list of ISOs for a specific page. 106 | // 107 | // Please note that filters specified in opts are not taken into account 108 | // when their value corresponds to their zero value or when they are empty. 109 | func (c *ISOClient) List(ctx context.Context, opts ISOListOpts) ([]*ISO, *Response, error) { 110 | const opPath = "/isos?%s" 111 | ctx = ctxutil.SetOpPath(ctx, opPath) 112 | 113 | reqPath := fmt.Sprintf(opPath, opts.values().Encode()) 114 | 115 | respBody, resp, err := getRequest[schema.ISOListResponse](ctx, c.client, reqPath) 116 | if err != nil { 117 | return nil, resp, err 118 | } 119 | 120 | return allFromSchemaFunc(respBody.ISOs, ISOFromSchema), resp, nil 121 | } 122 | 123 | // All returns all ISOs. 124 | func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) { 125 | return c.AllWithOpts(ctx, ISOListOpts{ListOpts: ListOpts{PerPage: 50}}) 126 | } 127 | 128 | // AllWithOpts returns all ISOs for the given options. 129 | func (c *ISOClient) AllWithOpts(ctx context.Context, opts ISOListOpts) ([]*ISO, error) { 130 | return iterPages(func(page int) ([]*ISO, *Response, error) { 131 | opts.Page = page 132 | return c.List(ctx, opts) 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /hcloud/labels.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | var keyRegexp = regexp.MustCompile( 9 | `^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,61}[a-z0-9A-Z])?$`) 10 | var valueRegexp = regexp.MustCompile(`^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[a-z0-9A-Z]$|$)`) 11 | 12 | func ValidateResourceLabels(labels map[string]interface{}) (bool, error) { 13 | for k, v := range labels { 14 | if match := keyRegexp.MatchString(k); !match { 15 | return false, fmt.Errorf("label key '%s' is not correctly formatted", k) 16 | } 17 | 18 | if match := valueRegexp.MatchString(v.(string)); !match { 19 | return false, fmt.Errorf("label value '%s' (key: %s) is not correctly formatted", v, k) 20 | } 21 | } 22 | return true, nil 23 | } 24 | -------------------------------------------------------------------------------- /hcloud/labels_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCheckLabels(t *testing.T) { 10 | t.Run("correct labels", func(t *testing.T) { 11 | labelMap := map[string]interface{}{ 12 | "label1": "correct.de", 13 | "label2.de/hallo": "1correct2.de", 14 | "label3-test.de/hallo.welt": "233344444443", 15 | "d/d": "d", 16 | "empty/label": "", 17 | } 18 | 19 | ok, err := ValidateResourceLabels(labelMap) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | assert.True(t, ok) 24 | }) 25 | t.Run("incorrect string label values", func(t *testing.T) { 26 | incorrectLabels := []string{ 27 | "incorrect .com", 28 | "-incorrect.com", 29 | "incorrect.com-", 30 | "incorr,ect.com-", 31 | "incorrect-111111111111111111111111111111111111111111111111111111111111.com", 32 | "63-characters-are-allowed-in-a-label__this-is-one-character-more", 33 | } 34 | 35 | for _, label := range incorrectLabels { 36 | labelMap := map[string]interface{}{ 37 | "test1": "valid", 38 | "test2": label, 39 | } 40 | ok, err := ValidateResourceLabels(labelMap) 41 | assert.Error(t, err) 42 | assert.False(t, ok) 43 | } 44 | }) 45 | t.Run("incorrect string label keys", func(t *testing.T) { 46 | incorrectLabels := []string{ 47 | "incorrect.de/", 48 | "incor rect.de/", 49 | "incorrect.de/+", 50 | "-incorrect.de", 51 | "incorrect.de-", 52 | "incorrect.de/tes t", 53 | "incorrect.de/test-", 54 | "incorrect.de/test-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", 55 | "incorrect-11111111111111111111111111111111111111111111111111111111111111111111111111111111" + 56 | "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + 57 | "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + 58 | "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + 59 | ".de/test", 60 | } 61 | 62 | for _, label := range incorrectLabels { 63 | labelMap := map[string]interface{}{ 64 | label: "cor-rect.de", 65 | } 66 | ok, err := ValidateResourceLabels(labelMap) 67 | assert.Error(t, err) 68 | assert.False(t, ok) 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /hcloud/load_balancer_type.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | 9 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 10 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 11 | ) 12 | 13 | // LoadBalancerType represents a LoadBalancer type in the Hetzner Cloud. 14 | type LoadBalancerType struct { 15 | ID int64 16 | Name string 17 | Description string 18 | MaxConnections int 19 | MaxServices int 20 | MaxTargets int 21 | MaxAssignedCertificates int 22 | Pricings []LoadBalancerTypeLocationPricing 23 | Deprecated *string 24 | } 25 | 26 | // LoadBalancerTypeClient is a client for the Load Balancer types API. 27 | type LoadBalancerTypeClient struct { 28 | client *Client 29 | } 30 | 31 | // GetByID retrieves a Load Balancer type by its ID. If the Load Balancer type does not exist, nil is returned. 32 | func (c *LoadBalancerTypeClient) GetByID(ctx context.Context, id int64) (*LoadBalancerType, *Response, error) { 33 | const opPath = "/load_balancer_types/%d" 34 | ctx = ctxutil.SetOpPath(ctx, opPath) 35 | 36 | reqPath := fmt.Sprintf(opPath, id) 37 | 38 | respBody, resp, err := getRequest[schema.LoadBalancerTypeGetResponse](ctx, c.client, reqPath) 39 | if err != nil { 40 | if IsError(err, ErrorCodeNotFound) { 41 | return nil, resp, nil 42 | } 43 | return nil, resp, err 44 | } 45 | 46 | return LoadBalancerTypeFromSchema(respBody.LoadBalancerType), resp, nil 47 | } 48 | 49 | // GetByName retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned. 50 | func (c *LoadBalancerTypeClient) GetByName(ctx context.Context, name string) (*LoadBalancerType, *Response, error) { 51 | return firstByName(name, func() ([]*LoadBalancerType, *Response, error) { 52 | return c.List(ctx, LoadBalancerTypeListOpts{Name: name}) 53 | }) 54 | } 55 | 56 | // Get retrieves a Load Balancer type by its ID if the input can be parsed as an integer, otherwise it 57 | // retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned. 58 | func (c *LoadBalancerTypeClient) Get(ctx context.Context, idOrName string) (*LoadBalancerType, *Response, error) { 59 | if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil { 60 | return c.GetByID(ctx, id) 61 | } 62 | return c.GetByName(ctx, idOrName) 63 | } 64 | 65 | // LoadBalancerTypeListOpts specifies options for listing Load Balancer types. 66 | type LoadBalancerTypeListOpts struct { 67 | ListOpts 68 | Name string 69 | Sort []string 70 | } 71 | 72 | func (l LoadBalancerTypeListOpts) values() url.Values { 73 | vals := l.ListOpts.Values() 74 | if l.Name != "" { 75 | vals.Add("name", l.Name) 76 | } 77 | for _, sort := range l.Sort { 78 | vals.Add("sort", sort) 79 | } 80 | return vals 81 | } 82 | 83 | // List returns a list of Load Balancer types for a specific page. 84 | // 85 | // Please note that filters specified in opts are not taken into account 86 | // when their value corresponds to their zero value or when they are empty. 87 | func (c *LoadBalancerTypeClient) List(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, *Response, error) { 88 | const opPath = "/load_balancer_types?%s" 89 | ctx = ctxutil.SetOpPath(ctx, opPath) 90 | 91 | reqPath := fmt.Sprintf(opPath, opts.values().Encode()) 92 | 93 | respBody, resp, err := getRequest[schema.LoadBalancerTypeListResponse](ctx, c.client, reqPath) 94 | if err != nil { 95 | return nil, resp, err 96 | } 97 | 98 | return allFromSchemaFunc(respBody.LoadBalancerTypes, LoadBalancerTypeFromSchema), resp, nil 99 | } 100 | 101 | // All returns all Load Balancer types. 102 | func (c *LoadBalancerTypeClient) All(ctx context.Context) ([]*LoadBalancerType, error) { 103 | return c.AllWithOpts(ctx, LoadBalancerTypeListOpts{ListOpts: ListOpts{PerPage: 50}}) 104 | } 105 | 106 | // AllWithOpts returns all Load Balancer types for the given options. 107 | func (c *LoadBalancerTypeClient) AllWithOpts(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, error) { 108 | return iterPages(func(page int) ([]*LoadBalancerType, *Response, error) { 109 | opts.Page = page 110 | return c.List(ctx, opts) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /hcloud/location.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | 9 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 10 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 11 | ) 12 | 13 | // Location represents a location in the Hetzner Cloud. 14 | type Location struct { 15 | ID int64 16 | Name string 17 | Description string 18 | Country string 19 | City string 20 | Latitude float64 21 | Longitude float64 22 | NetworkZone NetworkZone 23 | } 24 | 25 | // LocationClient is a client for the location API. 26 | type LocationClient struct { 27 | client *Client 28 | } 29 | 30 | // GetByID retrieves a location by its ID. If the location does not exist, nil is returned. 31 | func (c *LocationClient) GetByID(ctx context.Context, id int64) (*Location, *Response, error) { 32 | const opPath = "/locations/%d" 33 | ctx = ctxutil.SetOpPath(ctx, opPath) 34 | 35 | reqPath := fmt.Sprintf(opPath, id) 36 | 37 | respBody, resp, err := getRequest[schema.LocationGetResponse](ctx, c.client, reqPath) 38 | if err != nil { 39 | if IsError(err, ErrorCodeNotFound) { 40 | return nil, resp, nil 41 | } 42 | return nil, resp, err 43 | } 44 | 45 | return LocationFromSchema(respBody.Location), resp, nil 46 | } 47 | 48 | // GetByName retrieves an location by its name. If the location does not exist, nil is returned. 49 | func (c *LocationClient) GetByName(ctx context.Context, name string) (*Location, *Response, error) { 50 | return firstByName(name, func() ([]*Location, *Response, error) { 51 | return c.List(ctx, LocationListOpts{Name: name}) 52 | }) 53 | } 54 | 55 | // Get retrieves a location by its ID if the input can be parsed as an integer, otherwise it 56 | // retrieves a location by its name. If the location does not exist, nil is returned. 57 | func (c *LocationClient) Get(ctx context.Context, idOrName string) (*Location, *Response, error) { 58 | if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil { 59 | return c.GetByID(ctx, id) 60 | } 61 | return c.GetByName(ctx, idOrName) 62 | } 63 | 64 | // LocationListOpts specifies options for listing location. 65 | type LocationListOpts struct { 66 | ListOpts 67 | Name string 68 | Sort []string 69 | } 70 | 71 | func (l LocationListOpts) values() url.Values { 72 | vals := l.ListOpts.Values() 73 | if l.Name != "" { 74 | vals.Add("name", l.Name) 75 | } 76 | for _, sort := range l.Sort { 77 | vals.Add("sort", sort) 78 | } 79 | return vals 80 | } 81 | 82 | // List returns a list of locations for a specific page. 83 | // 84 | // Please note that filters specified in opts are not taken into account 85 | // when their value corresponds to their zero value or when they are empty. 86 | func (c *LocationClient) List(ctx context.Context, opts LocationListOpts) ([]*Location, *Response, error) { 87 | const opPath = "/locations?%s" 88 | ctx = ctxutil.SetOpPath(ctx, opPath) 89 | 90 | reqPath := fmt.Sprintf(opPath, opts.values().Encode()) 91 | 92 | respBody, resp, err := getRequest[schema.LocationListResponse](ctx, c.client, reqPath) 93 | if err != nil { 94 | return nil, resp, err 95 | } 96 | 97 | return allFromSchemaFunc(respBody.Locations, LocationFromSchema), resp, nil 98 | } 99 | 100 | // All returns all locations. 101 | func (c *LocationClient) All(ctx context.Context) ([]*Location, error) { 102 | return c.AllWithOpts(ctx, LocationListOpts{ListOpts: ListOpts{PerPage: 50}}) 103 | } 104 | 105 | // AllWithOpts returns all locations for the given options. 106 | func (c *LocationClient) AllWithOpts(ctx context.Context, opts LocationListOpts) ([]*Location, error) { 107 | return iterPages(func(page int) ([]*Location, *Response, error) { 108 | opts.Page = page 109 | return c.List(ctx, opts) 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /hcloud/metadata/client.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/prometheus/client_golang/prometheus" 15 | 16 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 17 | "github.com/hetznercloud/hcloud-go/v2/hcloud/internal/instrumentation" 18 | ) 19 | 20 | const Endpoint = "http://169.254.169.254/hetzner/v1/metadata" 21 | 22 | // Client is a client for the Hetzner Cloud Server Metadata Endpoints. 23 | type Client struct { 24 | endpoint string 25 | timeout time.Duration 26 | 27 | httpClient *http.Client 28 | instrumentationRegistry prometheus.Registerer 29 | } 30 | 31 | // A ClientOption is used to configure a [Client]. 32 | type ClientOption func(*Client) 33 | 34 | // WithEndpoint configures a [Client] to use the specified Metadata API endpoint. 35 | func WithEndpoint(endpoint string) ClientOption { 36 | return func(client *Client) { 37 | client.endpoint = strings.TrimRight(endpoint, "/") 38 | } 39 | } 40 | 41 | // WithHTTPClient configures a [Client] to perform HTTP requests with httpClient. 42 | func WithHTTPClient(httpClient *http.Client) ClientOption { 43 | return func(client *Client) { 44 | client.httpClient = httpClient 45 | } 46 | } 47 | 48 | // WithInstrumentation configures a [Client] to collect metrics about the performed HTTP requests. 49 | func WithInstrumentation(registry prometheus.Registerer) ClientOption { 50 | return func(client *Client) { 51 | client.instrumentationRegistry = registry 52 | } 53 | } 54 | 55 | // WithTimeout specifies a time limit for requests made by this [Client]. Defaults to 5 seconds. 56 | func WithTimeout(timeout time.Duration) ClientOption { 57 | return func(client *Client) { 58 | client.timeout = timeout 59 | } 60 | } 61 | 62 | // NewClient creates a new [Client] with the options applied. 63 | func NewClient(options ...ClientOption) *Client { 64 | client := &Client{ 65 | endpoint: Endpoint, 66 | httpClient: &http.Client{}, 67 | timeout: 5 * time.Second, 68 | } 69 | 70 | for _, option := range options { 71 | option(client) 72 | } 73 | 74 | client.httpClient.Timeout = client.timeout 75 | 76 | if client.instrumentationRegistry != nil { 77 | i := instrumentation.New("metadata", client.instrumentationRegistry) 78 | client.httpClient.Transport = i.InstrumentedRoundTripper(client.httpClient.Transport) 79 | } 80 | return client 81 | } 82 | 83 | // get executes an HTTP request against the API. 84 | func (c *Client) get(path string) (string, error) { 85 | ctx := ctxutil.SetOpPath(context.Background(), path) 86 | 87 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint+path, http.NoBody) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | resp, err := c.httpClient.Do(req) 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | defer resp.Body.Close() 98 | bodyBytes, err := io.ReadAll(resp.Body) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | body := string(bytes.TrimSpace(bodyBytes)) 104 | 105 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 106 | return body, fmt.Errorf("response status was %d", resp.StatusCode) 107 | } 108 | return body, nil 109 | } 110 | 111 | // IsHcloudServer checks if the currently called server is a hcloud server by calling a metadata endpoint 112 | // if the endpoint answers with a non-empty value this method returns true, otherwise false. 113 | func (c *Client) IsHcloudServer() bool { 114 | hostname, err := c.Hostname() 115 | if err != nil { 116 | return false 117 | } 118 | if len(hostname) > 0 { 119 | return true 120 | } 121 | return false 122 | } 123 | 124 | // Hostname returns the hostname of the server that did the request to the Metadata server. 125 | func (c *Client) Hostname() (string, error) { 126 | return c.get("/hostname") 127 | } 128 | 129 | // InstanceID returns the ID of the server that did the request to the Metadata server. 130 | func (c *Client) InstanceID() (int64, error) { 131 | resp, err := c.get("/instance-id") 132 | if err != nil { 133 | return 0, err 134 | } 135 | return strconv.ParseInt(resp, 10, 64) 136 | } 137 | 138 | // PublicIPv4 returns the Public IPv4 of the server that did the request to the Metadata server. 139 | func (c *Client) PublicIPv4() (net.IP, error) { 140 | resp, err := c.get("/public-ipv4") 141 | if err != nil { 142 | return nil, err 143 | } 144 | return net.ParseIP(resp), nil 145 | } 146 | 147 | // Region returns the Network Zone of the server that did the request to the Metadata server. 148 | func (c *Client) Region() (string, error) { 149 | return c.get("/region") 150 | } 151 | 152 | // AvailabilityZone returns the datacenter of the server that did the request to the Metadata server. 153 | func (c *Client) AvailabilityZone() (string, error) { 154 | return c.get("/availability-zone") 155 | } 156 | 157 | // PrivateNetworks returns details about the private networks the server is attached to. 158 | // Returns YAML (unparsed). 159 | func (c *Client) PrivateNetworks() (string, error) { 160 | return c.get("/private-networks") 161 | } 162 | -------------------------------------------------------------------------------- /hcloud/mocked_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/mockutil" 8 | ) 9 | 10 | type MockedTestCase struct { 11 | Name string 12 | WantRequests []mockutil.Request 13 | Run func(env testEnv) 14 | } 15 | 16 | func RunMockedTestCases(t *testing.T, testCases []MockedTestCase) { 17 | for _, testCase := range testCases { 18 | t.Run(testCase.Name, func(t *testing.T) { 19 | testServer := httptest.NewServer(mockutil.Handler(t, testCase.WantRequests)) 20 | 21 | env := newTestEnvWithServer(testServer, nil) 22 | defer env.Teardown() 23 | 24 | testCase.Run(env) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hcloud/pricing.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 7 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 8 | ) 9 | 10 | // Pricing specifies pricing information for various resources. 11 | type Pricing struct { 12 | Image ImagePricing 13 | // Deprecated: [Pricing.FloatingIP] is deprecated, use [Pricing.FloatingIPs] instead. 14 | FloatingIP FloatingIPPricing 15 | FloatingIPs []FloatingIPTypePricing 16 | PrimaryIPs []PrimaryIPPricing 17 | // Deprecated: [Pricing.Traffic] is deprecated and will report 0 after 2024-08-05. 18 | // Use traffic pricing from [Pricing.ServerTypes] or [Pricing.LoadBalancerTypes] instead. 19 | Traffic TrafficPricing 20 | ServerBackup ServerBackupPricing 21 | ServerTypes []ServerTypePricing 22 | LoadBalancerTypes []LoadBalancerTypePricing 23 | Volume VolumePricing 24 | } 25 | 26 | // Price represents a price. Net amount, gross amount, as well as VAT rate are 27 | // specified as strings and it is the user's responsibility to convert them to 28 | // appropriate types for calculations. 29 | type Price struct { 30 | Currency string 31 | VATRate string 32 | Net string 33 | Gross string 34 | } 35 | 36 | // PrimaryIPPrice represents a price. Net amount and gross amount are 37 | // specified as strings and it is the user's responsibility to convert them to 38 | // appropriate types for calculations. 39 | type PrimaryIPPrice struct { 40 | Net string 41 | Gross string 42 | } 43 | 44 | // ImagePricing provides pricing information for imaegs. 45 | type ImagePricing struct { 46 | PerGBMonth Price 47 | } 48 | 49 | // FloatingIPPricing provides pricing information for Floating IPs. 50 | type FloatingIPPricing struct { 51 | Monthly Price 52 | } 53 | 54 | // FloatingIPTypePricing provides pricing information for Floating IPs per Type. 55 | type FloatingIPTypePricing struct { 56 | Type FloatingIPType 57 | Pricings []FloatingIPTypeLocationPricing 58 | } 59 | 60 | // PrimaryIPTypePricing defines the schema of pricing information for a primary IP 61 | // type at a datacenter. 62 | type PrimaryIPTypePricing struct { 63 | Datacenter string // Deprecated: the API does not return pricing for the individual DCs anymore 64 | Location string 65 | Hourly PrimaryIPPrice 66 | Monthly PrimaryIPPrice 67 | } 68 | 69 | // PrimaryIPTypePricing provides pricing information for PrimaryIPs. 70 | type PrimaryIPPricing struct { 71 | Type string 72 | Pricings []PrimaryIPTypePricing 73 | } 74 | 75 | // FloatingIPTypeLocationPricing provides pricing information for a Floating IP type 76 | // at a location. 77 | type FloatingIPTypeLocationPricing struct { 78 | Location *Location 79 | Monthly Price 80 | } 81 | 82 | // TrafficPricing provides pricing information for traffic. 83 | type TrafficPricing struct { 84 | PerTB Price 85 | } 86 | 87 | // VolumePricing provides pricing information for a Volume. 88 | type VolumePricing struct { 89 | PerGBMonthly Price 90 | } 91 | 92 | // ServerBackupPricing provides pricing information for server backups. 93 | type ServerBackupPricing struct { 94 | Percentage string 95 | } 96 | 97 | // ServerTypePricing provides pricing information for a server type. 98 | type ServerTypePricing struct { 99 | ServerType *ServerType 100 | Pricings []ServerTypeLocationPricing 101 | } 102 | 103 | // ServerTypeLocationPricing provides pricing information for a server type 104 | // at a location. 105 | type ServerTypeLocationPricing struct { 106 | Location *Location 107 | Hourly Price 108 | Monthly Price 109 | 110 | // IncludedTraffic is the free traffic per month in bytes 111 | IncludedTraffic uint64 112 | PerTBTraffic Price 113 | } 114 | 115 | // LoadBalancerTypePricing provides pricing information for a Load Balancer type. 116 | type LoadBalancerTypePricing struct { 117 | LoadBalancerType *LoadBalancerType 118 | Pricings []LoadBalancerTypeLocationPricing 119 | } 120 | 121 | // LoadBalancerTypeLocationPricing provides pricing information for a Load Balancer type 122 | // at a location. 123 | type LoadBalancerTypeLocationPricing struct { 124 | Location *Location 125 | Hourly Price 126 | Monthly Price 127 | 128 | // IncludedTraffic is the free traffic per month in bytes 129 | IncludedTraffic uint64 130 | PerTBTraffic Price 131 | } 132 | 133 | // PricingClient is a client for the pricing API. 134 | type PricingClient struct { 135 | client *Client 136 | } 137 | 138 | // Get retrieves pricing information. 139 | func (c *PricingClient) Get(ctx context.Context) (Pricing, *Response, error) { 140 | const opPath = "/pricing" 141 | ctx = ctxutil.SetOpPath(ctx, opPath) 142 | 143 | reqPath := opPath 144 | 145 | respBody, resp, err := getRequest[schema.PricingGetResponse](ctx, c.client, reqPath) 146 | if err != nil { 147 | return Pricing{}, resp, err 148 | } 149 | 150 | return PricingFromSchema(respBody.Pricing), resp, nil 151 | } 152 | -------------------------------------------------------------------------------- /hcloud/pricing_test.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 10 | ) 11 | 12 | func TestPricingClientGet(t *testing.T) { 13 | env := newTestEnv() 14 | defer env.Teardown() 15 | 16 | env.Mux.HandleFunc("/pricing", func(w http.ResponseWriter, r *http.Request) { 17 | json.NewEncoder(w).Encode(schema.PricingGetResponse{ 18 | Pricing: schema.Pricing{ 19 | Currency: "EUR", 20 | }, 21 | }) 22 | }) 23 | ctx := context.Background() 24 | 25 | pricing, _, err := env.Client.Pricing.Get(ctx) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if pricing.Image.PerGBMonth.Currency != "EUR" { 30 | t.Errorf("unexpected currency: %v", pricing.Image.PerGBMonth.Currency) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hcloud/rdns.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | // RDNSSupporter defines functions to change and lookup reverse dns entries. 10 | // currently implemented by Server, FloatingIP, PrimaryIP and LoadBalancer. 11 | type RDNSSupporter interface { 12 | // changeDNSPtr changes or resets the reverse DNS pointer for a IP address. 13 | // Pass a nil ptr to reset the reverse DNS pointer to its default value. 14 | changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) 15 | // GetDNSPtrForIP searches for the dns assigned to the given IP address. 16 | // It returns an error if there is no dns set for the given IP address. 17 | GetDNSPtrForIP(ip net.IP) (string, error) 18 | } 19 | 20 | // RDNSClient simplifies the handling objects which support reverse dns entries. 21 | type RDNSClient struct { 22 | client *Client 23 | } 24 | 25 | // ChangeDNSPtr changes or resets the reverse DNS pointer for a IP address. 26 | // Pass a nil ptr to reset the reverse DNS pointer to its default value. 27 | func (c *RDNSClient) ChangeDNSPtr(ctx context.Context, rdns RDNSSupporter, ip net.IP, ptr *string) (*Action, *Response, error) { 28 | return rdns.changeDNSPtr(ctx, c.client, ip, ptr) 29 | } 30 | 31 | // SupportsRDNS checks if the object supports reverse dns functions. 32 | func SupportsRDNS(i interface{}) bool { 33 | _, ok := i.(RDNSSupporter) 34 | return ok 35 | } 36 | 37 | // RDNSLookup searches for the dns assigned to the given IP address. 38 | // It returns an error if the object does not support reverse dns or if there is no dns set for the given IP address. 39 | func RDNSLookup(i interface{}, ip net.IP) (string, error) { 40 | rdns, ok := i.(RDNSSupporter) 41 | if !ok { 42 | return "", fmt.Errorf("%+v does not support RDNS", i) 43 | } 44 | 45 | return rdns.GetDNSPtrForIP(ip) 46 | } 47 | 48 | // Make sure that all expected Resources actually implement the interface. 49 | var _ RDNSSupporter = &FloatingIP{} 50 | var _ RDNSSupporter = &PrimaryIP{} 51 | var _ RDNSSupporter = &Server{} 52 | var _ RDNSSupporter = &LoadBalancer{} 53 | -------------------------------------------------------------------------------- /hcloud/resource.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | // Resource defines the schema of a resource. 4 | type Resource struct { 5 | ID int64 6 | Type string 7 | } 8 | -------------------------------------------------------------------------------- /hcloud/schema/README.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | The [`schema`](./) package holds API schemas for the `hcloud-go` library. 4 | 5 | > [!CAUTION] 6 | > Breaking changes may occur without notice. Do not use in production! 7 | -------------------------------------------------------------------------------- /hcloud/schema/action.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // Action defines the schema of an action. 6 | type Action struct { 7 | ID int64 `json:"id"` 8 | Status string `json:"status"` 9 | Command string `json:"command"` 10 | Progress int `json:"progress"` 11 | Started time.Time `json:"started"` 12 | Finished *time.Time `json:"finished"` 13 | Error *ActionError `json:"error"` 14 | Resources []ActionResourceReference `json:"resources"` 15 | } 16 | 17 | // ActionResourceReference defines the schema of an action resource reference. 18 | type ActionResourceReference struct { 19 | ID int64 `json:"id"` 20 | Type string `json:"type"` 21 | } 22 | 23 | // ActionError defines the schema of an error embedded 24 | // in an action. 25 | type ActionError struct { 26 | Code string `json:"code"` 27 | Message string `json:"message"` 28 | } 29 | 30 | // ActionGetResponse is the schema of the response when 31 | // retrieving a single action. 32 | type ActionGetResponse struct { 33 | Action Action `json:"action"` 34 | } 35 | 36 | // ActionListResponse defines the schema of the response when listing actions. 37 | type ActionListResponse struct { 38 | Actions []Action `json:"actions"` 39 | } 40 | -------------------------------------------------------------------------------- /hcloud/schema/certificate.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // CertificateUsedByRef defines the schema of a resource using a certificate. 6 | type CertificateUsedByRef struct { 7 | ID int64 `json:"id"` 8 | Type string `json:"type"` 9 | } 10 | 11 | type CertificateStatusRef struct { 12 | Issuance string `json:"issuance"` 13 | Renewal string `json:"renewal"` 14 | Error *Error `json:"error,omitempty"` 15 | } 16 | 17 | // Certificate defines the schema of an certificate. 18 | type Certificate struct { 19 | ID int64 `json:"id"` 20 | Name string `json:"name"` 21 | Labels map[string]string `json:"labels"` 22 | Type string `json:"type"` 23 | Certificate string `json:"certificate"` 24 | Created time.Time `json:"created"` 25 | NotValidBefore time.Time `json:"not_valid_before"` 26 | NotValidAfter time.Time `json:"not_valid_after"` 27 | DomainNames []string `json:"domain_names"` 28 | Fingerprint string `json:"fingerprint"` 29 | Status *CertificateStatusRef `json:"status"` 30 | UsedBy []CertificateUsedByRef `json:"used_by"` 31 | } 32 | 33 | // CertificateListResponse defines the schema of the response when 34 | // listing Certificates. 35 | type CertificateListResponse struct { 36 | Certificates []Certificate `json:"certificates"` 37 | } 38 | 39 | // CertificateGetResponse defines the schema of the response when 40 | // retrieving a single Certificate. 41 | type CertificateGetResponse struct { 42 | Certificate Certificate `json:"certificate"` 43 | } 44 | 45 | // CertificateCreateRequest defines the schema of the request to create a certificate. 46 | type CertificateCreateRequest struct { 47 | Name string `json:"name"` 48 | Type string `json:"type"` 49 | DomainNames []string `json:"domain_names,omitempty"` 50 | Certificate string `json:"certificate,omitempty"` 51 | PrivateKey string `json:"private_key,omitempty"` 52 | Labels *map[string]string `json:"labels,omitempty"` 53 | } 54 | 55 | // CertificateCreateResponse defines the schema of the response when creating a certificate. 56 | type CertificateCreateResponse struct { 57 | Certificate Certificate `json:"certificate"` 58 | Action *Action `json:"action"` 59 | } 60 | 61 | // CertificateUpdateRequest defines the schema of the request to update a certificate. 62 | type CertificateUpdateRequest struct { 63 | Name *string `json:"name,omitempty"` 64 | Labels *map[string]string `json:"labels,omitempty"` 65 | } 66 | 67 | // CertificateUpdateResponse defines the schema of the response when updating a certificate. 68 | type CertificateUpdateResponse struct { 69 | Certificate Certificate `json:"certificate"` 70 | } 71 | 72 | // CertificateIssuanceRetryResponse defines the schema for the response of the 73 | // retry issuance endpoint. 74 | type CertificateIssuanceRetryResponse struct { 75 | Action Action `json:"action"` 76 | } 77 | -------------------------------------------------------------------------------- /hcloud/schema/datacenter.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // Datacenter defines the schema of a datacenter. 4 | type Datacenter struct { 5 | ID int64 `json:"id"` 6 | Name string `json:"name"` 7 | Description string `json:"description"` 8 | Location Location `json:"location"` 9 | ServerTypes DatacenterServerTypes `json:"server_types"` 10 | } 11 | 12 | // DatacenterServerTypes defines the schema of the server types available in a datacenter. 13 | type DatacenterServerTypes struct { 14 | Supported []int64 `json:"supported"` 15 | AvailableForMigration []int64 `json:"available_for_migration"` 16 | Available []int64 `json:"available"` 17 | } 18 | 19 | // DatacenterGetResponse defines the schema of the response when retrieving a single datacenter. 20 | type DatacenterGetResponse struct { 21 | Datacenter Datacenter `json:"datacenter"` 22 | } 23 | 24 | // DatacenterListResponse defines the schema of the response when listing datacenters. 25 | type DatacenterListResponse struct { 26 | Datacenters []Datacenter `json:"datacenters"` 27 | } 28 | -------------------------------------------------------------------------------- /hcloud/schema/deprecation.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | type DeprecationInfo struct { 6 | Announced time.Time `json:"announced"` 7 | UnavailableAfter time.Time `json:"unavailable_after"` 8 | } 9 | 10 | type DeprecatableResource struct { 11 | Deprecation *DeprecationInfo `json:"deprecation"` 12 | } 13 | -------------------------------------------------------------------------------- /hcloud/schema/doc.go: -------------------------------------------------------------------------------- 1 | // The schema package holds API schemas for the `hcloud-go` library. 2 | 3 | // Breaking changes may occur without notice. Do not use in production! 4 | package schema 5 | -------------------------------------------------------------------------------- /hcloud/schema/error.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "encoding/json" 4 | 5 | // Error represents the schema of an error response. 6 | type Error struct { 7 | Code string `json:"code"` 8 | Message string `json:"message"` 9 | DetailsRaw json.RawMessage `json:"details"` 10 | Details any `json:"-"` 11 | } 12 | 13 | // UnmarshalJSON overrides default json unmarshalling. 14 | func (e *Error) UnmarshalJSON(data []byte) (err error) { 15 | type Alias Error 16 | alias := (*Alias)(e) 17 | if err = json.Unmarshal(data, alias); err != nil { 18 | return 19 | } 20 | if e.Code == "invalid_input" && len(e.DetailsRaw) > 0 { 21 | details := ErrorDetailsInvalidInput{} 22 | if err = json.Unmarshal(e.DetailsRaw, &details); err != nil { 23 | return 24 | } 25 | alias.Details = details 26 | } 27 | if e.Code == "deprecated_api_endpoint" && len(e.DetailsRaw) > 0 { 28 | details := ErrorDetailsDeprecatedAPIEndpoint{} 29 | if err = json.Unmarshal(e.DetailsRaw, &details); err != nil { 30 | return 31 | } 32 | alias.Details = details 33 | } 34 | return 35 | } 36 | 37 | // ErrorResponse defines the schema of a response containing an error. 38 | type ErrorResponse struct { 39 | Error Error `json:"error"` 40 | } 41 | 42 | // ErrorDetailsInvalidInput defines the schema of the Details field 43 | // of an error with code 'invalid_input'. 44 | type ErrorDetailsInvalidInput struct { 45 | Fields []struct { 46 | Name string `json:"name"` 47 | Messages []string `json:"messages"` 48 | } `json:"fields"` 49 | } 50 | 51 | // ErrorDetailsDeprecatedAPIEndpoint defines the schema of the Details field 52 | // of an error with code 'deprecated_api_endpoint'. 53 | type ErrorDetailsDeprecatedAPIEndpoint struct { 54 | Announcement string `json:"announcement"` 55 | } 56 | -------------------------------------------------------------------------------- /hcloud/schema/error_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestInvalidInputError(t *testing.T) { 9 | t.Run("UnmarshalJSON", func(t *testing.T) { 10 | data := []byte(`{ 11 | "code": "invalid_input", 12 | "message": "invalid input", 13 | "details": { 14 | "fields": [ 15 | { 16 | "name": "broken_field", 17 | "messages": ["is required"] 18 | } 19 | ] 20 | } 21 | }`) 22 | 23 | e := &Error{} 24 | err := json.Unmarshal(data, e) 25 | if err != nil { 26 | t.Fatalf("unexpected error: %v", err) 27 | } 28 | if e.Code != "invalid_input" { 29 | t.Errorf("unexpected Code: %v", e.Code) 30 | } 31 | if e.Message != "invalid input" { 32 | t.Errorf("unexpected Message: %v", e.Message) 33 | } 34 | if e.Details == nil { 35 | t.Fatalf("unexpected Details: %v", e.Details) 36 | } 37 | d, ok := e.Details.(ErrorDetailsInvalidInput) 38 | if !ok { 39 | t.Fatalf("unexpected Details type (should be ErrorDetailsInvalidInput): %v", e.Details) 40 | } 41 | if len(d.Fields) != 1 { 42 | t.Fatalf("unexpected Details.Fields length (should be 1): %v", d.Fields) 43 | } 44 | if d.Fields[0].Name != "broken_field" { 45 | t.Errorf("unexpected Details.Fields[0].Name: %v", d.Fields[0].Name) 46 | } 47 | if len(d.Fields[0].Messages) != 1 { 48 | t.Fatalf("unexpected Details.Fields[0].Messages length (should be 1): %v", d.Fields[0].Messages) 49 | } 50 | if d.Fields[0].Messages[0] != "is required" { 51 | t.Errorf("unexpected Details.Fields[0].Messages[0]: %v", d.Fields[0].Messages[0]) 52 | } 53 | }) 54 | } 55 | 56 | func TestDeprecatedAPIEndpointError(t *testing.T) { 57 | t.Run("UnmarshalJSON", func(t *testing.T) { 58 | data := []byte(`{ 59 | "code": "deprecated_api_endpoint", 60 | "message": "API functionality was removed", 61 | "details": { 62 | "announcement": "https://docs.hetzner.cloud/changelog#2023-07-20-foo-endpoint-is-deprecated" 63 | } 64 | }`) 65 | 66 | e := &Error{} 67 | err := json.Unmarshal(data, e) 68 | if err != nil { 69 | t.Fatalf("unexpected error: %v", err) 70 | } 71 | 72 | if e.Details == nil { 73 | t.Fatalf("unexpected Details: %v", e.Details) 74 | } 75 | d, ok := e.Details.(ErrorDetailsDeprecatedAPIEndpoint) 76 | if !ok { 77 | t.Fatalf("unexpected Details type (should be ErrorDetailsDeprecatedAPIEndpoint): %v", e.Details) 78 | } 79 | if d.Announcement != "https://docs.hetzner.cloud/changelog#2023-07-20-foo-endpoint-is-deprecated" { 80 | t.Fatalf("unexpected Details.Announcement: %v", d.Announcement) 81 | } 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /hcloud/schema/firewall.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // Firewall defines the schema of a Firewall. 6 | type Firewall struct { 7 | ID int64 `json:"id"` 8 | Name string `json:"name"` 9 | Labels map[string]string `json:"labels"` 10 | Created time.Time `json:"created"` 11 | Rules []FirewallRule `json:"rules"` 12 | AppliedTo []FirewallResource `json:"applied_to"` 13 | } 14 | 15 | // FirewallRule defines the schema of a Firewall rule in responses. 16 | type FirewallRule struct { 17 | Direction string `json:"direction"` 18 | SourceIPs []string `json:"source_ips"` 19 | DestinationIPs []string `json:"destination_ips"` 20 | Protocol string `json:"protocol"` 21 | Port *string `json:"port"` 22 | Description *string `json:"description"` 23 | } 24 | 25 | // FirewallRuleRequest defines the schema of a Firewall rule in requests. 26 | type FirewallRuleRequest struct { 27 | Direction string `json:"direction"` 28 | SourceIPs []string `json:"source_ips,omitempty"` 29 | DestinationIPs []string `json:"destination_ips,omitempty"` 30 | Protocol string `json:"protocol"` 31 | Port *string `json:"port,omitempty"` 32 | Description *string `json:"description,omitempty"` 33 | } 34 | 35 | // FirewallListResponse defines the schema of the response when listing Firewalls. 36 | type FirewallListResponse struct { 37 | Firewalls []Firewall `json:"firewalls"` 38 | } 39 | 40 | // FirewallGetResponse defines the schema of the response when retrieving a single Firewall. 41 | type FirewallGetResponse struct { 42 | Firewall Firewall `json:"firewall"` 43 | } 44 | 45 | // FirewallCreateRequest defines the schema of the request to create a Firewall. 46 | type FirewallCreateRequest struct { 47 | Name string `json:"name"` 48 | Labels *map[string]string `json:"labels,omitempty"` 49 | Rules []FirewallRuleRequest `json:"rules,omitempty"` 50 | ApplyTo []FirewallResource `json:"apply_to,omitempty"` 51 | } 52 | 53 | // FirewallResource defines the schema of a resource to apply the new Firewall on. 54 | type FirewallResource struct { 55 | Type string `json:"type"` 56 | Server *FirewallResourceServer `json:"server,omitempty"` 57 | LabelSelector *FirewallResourceLabelSelector `json:"label_selector,omitempty"` 58 | } 59 | 60 | // FirewallResourceLabelSelector defines the schema of a LabelSelector to apply a Firewall on. 61 | type FirewallResourceLabelSelector struct { 62 | Selector string `json:"selector"` 63 | } 64 | 65 | // FirewallResourceServer defines the schema of a Server to apply a Firewall on. 66 | type FirewallResourceServer struct { 67 | ID int64 `json:"id"` 68 | } 69 | 70 | // FirewallCreateResponse defines the schema of the response when creating a Firewall. 71 | type FirewallCreateResponse struct { 72 | Firewall Firewall `json:"firewall"` 73 | Actions []Action `json:"actions"` 74 | } 75 | 76 | // FirewallUpdateRequest defines the schema of the request to update a Firewall. 77 | type FirewallUpdateRequest struct { 78 | Name *string `json:"name,omitempty"` 79 | Labels *map[string]string `json:"labels,omitempty"` 80 | } 81 | 82 | // FirewallUpdateResponse defines the schema of the response when updating a Firewall. 83 | type FirewallUpdateResponse struct { 84 | Firewall Firewall `json:"firewall"` 85 | } 86 | 87 | // FirewallActionSetRulesRequest defines the schema of the request when setting Firewall rules. 88 | type FirewallActionSetRulesRequest struct { 89 | Rules []FirewallRuleRequest `json:"rules"` 90 | } 91 | 92 | // FirewallActionSetRulesResponse defines the schema of the response when setting Firewall rules. 93 | type FirewallActionSetRulesResponse struct { 94 | Actions []Action `json:"actions"` 95 | } 96 | 97 | // FirewallActionApplyToResourcesRequest defines the schema of the request when applying a Firewall on resources. 98 | type FirewallActionApplyToResourcesRequest struct { 99 | ApplyTo []FirewallResource `json:"apply_to"` 100 | } 101 | 102 | // FirewallActionApplyToResourcesResponse defines the schema of the response when applying a Firewall on resources. 103 | type FirewallActionApplyToResourcesResponse struct { 104 | Actions []Action `json:"actions"` 105 | } 106 | 107 | // FirewallActionRemoveFromResourcesRequest defines the schema of the request when removing a Firewall from resources. 108 | type FirewallActionRemoveFromResourcesRequest struct { 109 | RemoveFrom []FirewallResource `json:"remove_from"` 110 | } 111 | 112 | // FirewallActionRemoveFromResourcesResponse defines the schema of the response when removing a Firewall from resources. 113 | type FirewallActionRemoveFromResourcesResponse struct { 114 | Actions []Action `json:"actions"` 115 | } 116 | -------------------------------------------------------------------------------- /hcloud/schema/floating_ip.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // FloatingIP defines the schema of a Floating IP. 6 | type FloatingIP struct { 7 | ID int64 `json:"id"` 8 | Description *string `json:"description"` 9 | Created time.Time `json:"created"` 10 | IP string `json:"ip"` 11 | Type string `json:"type"` 12 | Server *int64 `json:"server"` 13 | DNSPtr []FloatingIPDNSPtr `json:"dns_ptr"` 14 | HomeLocation Location `json:"home_location"` 15 | Blocked bool `json:"blocked"` 16 | Protection FloatingIPProtection `json:"protection"` 17 | Labels map[string]string `json:"labels"` 18 | Name string `json:"name"` 19 | } 20 | 21 | // FloatingIPProtection represents the protection level of a Floating IP. 22 | type FloatingIPProtection struct { 23 | Delete bool `json:"delete"` 24 | } 25 | 26 | // FloatingIPDNSPtr contains reverse DNS information for a 27 | // IPv4 or IPv6 Floating IP. 28 | type FloatingIPDNSPtr struct { 29 | IP string `json:"ip"` 30 | DNSPtr string `json:"dns_ptr"` 31 | } 32 | 33 | // FloatingIPGetResponse defines the schema of the response when 34 | // retrieving a single Floating IP. 35 | type FloatingIPGetResponse struct { 36 | FloatingIP FloatingIP `json:"floating_ip"` 37 | } 38 | 39 | // FloatingIPUpdateRequest defines the schema of the request to update a Floating IP. 40 | type FloatingIPUpdateRequest struct { 41 | Description string `json:"description,omitempty"` 42 | Labels *map[string]string `json:"labels,omitempty"` 43 | Name string `json:"name,omitempty"` 44 | } 45 | 46 | // FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP. 47 | type FloatingIPUpdateResponse struct { 48 | FloatingIP FloatingIP `json:"floating_ip"` 49 | } 50 | 51 | // FloatingIPListResponse defines the schema of the response when 52 | // listing Floating IPs. 53 | type FloatingIPListResponse struct { 54 | FloatingIPs []FloatingIP `json:"floating_ips"` 55 | } 56 | 57 | // FloatingIPCreateRequest defines the schema of the request to 58 | // create a Floating IP. 59 | type FloatingIPCreateRequest struct { 60 | Type string `json:"type"` 61 | HomeLocation *string `json:"home_location,omitempty"` 62 | Server *int64 `json:"server,omitempty"` 63 | Description *string `json:"description,omitempty"` 64 | Labels *map[string]string `json:"labels,omitempty"` 65 | Name *string `json:"name,omitempty"` 66 | } 67 | 68 | // FloatingIPCreateResponse defines the schema of the response 69 | // when creating a Floating IP. 70 | type FloatingIPCreateResponse struct { 71 | FloatingIP FloatingIP `json:"floating_ip"` 72 | Action *Action `json:"action"` 73 | } 74 | 75 | // FloatingIPActionAssignRequest defines the schema of the request to 76 | // create an assign Floating IP action. 77 | type FloatingIPActionAssignRequest struct { 78 | Server int64 `json:"server"` 79 | } 80 | 81 | // FloatingIPActionAssignResponse defines the schema of the response when 82 | // creating an assign action. 83 | type FloatingIPActionAssignResponse struct { 84 | Action Action `json:"action"` 85 | } 86 | 87 | // FloatingIPActionUnassignRequest defines the schema of the request to 88 | // create an unassign Floating IP action. 89 | type FloatingIPActionUnassignRequest struct{} 90 | 91 | // FloatingIPActionUnassignResponse defines the schema of the response when 92 | // creating an unassign action. 93 | type FloatingIPActionUnassignResponse struct { 94 | Action Action `json:"action"` 95 | } 96 | 97 | // FloatingIPActionChangeDNSPtrRequest defines the schema for the request to 98 | // change a Floating IP's reverse DNS pointer. 99 | type FloatingIPActionChangeDNSPtrRequest struct { 100 | IP string `json:"ip"` 101 | DNSPtr *string `json:"dns_ptr"` 102 | } 103 | 104 | // FloatingIPActionChangeDNSPtrResponse defines the schema of the response when 105 | // creating a change_dns_ptr Floating IP action. 106 | type FloatingIPActionChangeDNSPtrResponse struct { 107 | Action Action `json:"action"` 108 | } 109 | 110 | // FloatingIPActionChangeProtectionRequest defines the schema of the request to change the resource protection of a Floating IP. 111 | type FloatingIPActionChangeProtectionRequest struct { 112 | Delete *bool `json:"delete,omitempty"` 113 | } 114 | 115 | // FloatingIPActionChangeProtectionResponse defines the schema of the response when changing the resource protection of a Floating IP. 116 | type FloatingIPActionChangeProtectionResponse struct { 117 | Action Action `json:"action"` 118 | } 119 | -------------------------------------------------------------------------------- /hcloud/schema/floating_ip_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestFloatingIPCreateRequest(t *testing.T) { 10 | var ( 11 | oneLabel = map[string]string{"foo": "bar"} 12 | nilLabels map[string]string 13 | emptyLabels = map[string]string{} 14 | ) 15 | 16 | testCases := []struct { 17 | name string 18 | in FloatingIPCreateRequest 19 | out []byte 20 | }{ 21 | { 22 | name: "no labels", 23 | in: FloatingIPCreateRequest{Type: "ipv4"}, 24 | out: []byte(`{"type":"ipv4"}`), 25 | }, 26 | { 27 | name: "one label", 28 | in: FloatingIPCreateRequest{Type: "ipv4", Labels: &oneLabel}, 29 | out: []byte(`{"type":"ipv4","labels":{"foo":"bar"}}`), 30 | }, 31 | { 32 | name: "nil labels", 33 | in: FloatingIPCreateRequest{Type: "ipv4", Labels: &nilLabels}, 34 | out: []byte(`{"type":"ipv4","labels":null}`), 35 | }, 36 | { 37 | name: "empty labels", 38 | in: FloatingIPCreateRequest{Type: "ipv4", Labels: &emptyLabels}, 39 | out: []byte(`{"type":"ipv4","labels":{}}`), 40 | }, 41 | } 42 | 43 | for _, testCase := range testCases { 44 | t.Run(testCase.name, func(t *testing.T) { 45 | data, err := json.Marshal(testCase.in) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !bytes.Equal(data, testCase.out) { 50 | t.Fatalf("output %s does not match %s", data, testCase.out) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hcloud/schema/id_or_name.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | // IDOrName can be used in API requests where either a resource id or name can be 11 | // specified. 12 | type IDOrName struct { 13 | ID int64 14 | Name string 15 | } 16 | 17 | var _ json.Unmarshaler = (*IDOrName)(nil) 18 | var _ json.Marshaler = (*IDOrName)(nil) 19 | 20 | func (o IDOrName) MarshalJSON() ([]byte, error) { 21 | if o.ID != 0 { 22 | return json.Marshal(o.ID) 23 | } 24 | if o.Name != "" { 25 | return json.Marshal(o.Name) 26 | } 27 | 28 | // We want to preserve the behavior of an empty interface{} to prevent breaking 29 | // changes (marshaled to null when empty). 30 | return json.Marshal(nil) 31 | } 32 | 33 | func (o *IDOrName) UnmarshalJSON(data []byte) error { 34 | d := json.NewDecoder(bytes.NewBuffer(data)) 35 | // This ensures we won't lose precision on large IDs, see json.Number below 36 | d.UseNumber() 37 | 38 | var v any 39 | if err := d.Decode(&v); err != nil { 40 | return err 41 | } 42 | 43 | switch typed := v.(type) { 44 | case string: 45 | id, err := strconv.ParseInt(typed, 10, 64) 46 | if err == nil { 47 | o.ID = id 48 | } else if typed != "" { 49 | o.Name = typed 50 | } 51 | case json.Number: 52 | id, err := typed.Int64() 53 | if err != nil { 54 | return &json.UnmarshalTypeError{ 55 | Value: string(data), 56 | Type: reflect.TypeOf(*o), 57 | } 58 | } 59 | o.ID = id 60 | default: 61 | return &json.UnmarshalTypeError{ 62 | Value: string(data), 63 | Type: reflect.TypeOf(*o), 64 | } 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /hcloud/schema/id_or_name_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIDOrNameMarshall(t *testing.T) { 11 | t.Run("id", func(t *testing.T) { 12 | i := IDOrName{ID: 1} 13 | 14 | got, err := i.MarshalJSON() 15 | require.NoError(t, err) 16 | require.Equal(t, `1`, string(got)) 17 | }) 18 | 19 | t.Run("name", func(t *testing.T) { 20 | i := IDOrName{Name: "name"} 21 | 22 | got, err := i.MarshalJSON() 23 | require.NoError(t, err) 24 | require.Equal(t, `"name"`, string(got)) 25 | }) 26 | 27 | t.Run("id and name", func(t *testing.T) { 28 | i := IDOrName{ID: 1, Name: "name"} 29 | 30 | got, err := i.MarshalJSON() 31 | require.NoError(t, err) 32 | require.Equal(t, `1`, string(got)) 33 | }) 34 | 35 | t.Run("null", func(t *testing.T) { 36 | i := IDOrName{} 37 | 38 | got, err := i.MarshalJSON() 39 | require.NoError(t, err) 40 | require.Equal(t, `null`, string(got)) 41 | }) 42 | } 43 | 44 | func TestIDOrNameUnMarshall(t *testing.T) { 45 | t.Run("id", func(t *testing.T) { 46 | i := IDOrName{} 47 | 48 | err := i.UnmarshalJSON([]byte(`1`)) 49 | require.NoError(t, err) 50 | require.Equal(t, IDOrName{ID: 1}, i) 51 | }) 52 | t.Run("name", func(t *testing.T) { 53 | i := IDOrName{} 54 | 55 | err := i.UnmarshalJSON([]byte(`"name"`)) 56 | require.NoError(t, err) 57 | require.Equal(t, IDOrName{Name: "name"}, i) 58 | }) 59 | t.Run("id string", func(t *testing.T) { 60 | i := IDOrName{} 61 | 62 | err := i.UnmarshalJSON([]byte(`"1"`)) 63 | require.NoError(t, err) 64 | require.Equal(t, IDOrName{ID: 1}, i) 65 | }) 66 | t.Run("id float", func(t *testing.T) { 67 | i := IDOrName{} 68 | 69 | err := i.UnmarshalJSON([]byte(`1.0`)) 70 | require.EqualError(t, err, "json: cannot unmarshal 1.0 into Go value of type schema.IDOrName") 71 | }) 72 | t.Run("null", func(t *testing.T) { 73 | i := IDOrName{} 74 | 75 | err := i.UnmarshalJSON([]byte(`null`)) 76 | require.EqualError(t, err, "json: cannot unmarshal null into Go value of type schema.IDOrName") 77 | }) 78 | } 79 | 80 | func TestIDOrName(t *testing.T) { 81 | // Make sure the behavior does not change from the use of an interface{}. 82 | type FakeRequest struct { 83 | Old any `json:"old"` 84 | New IDOrName `json:"new"` 85 | } 86 | 87 | t.Run("null", func(t *testing.T) { 88 | o := FakeRequest{} 89 | body, err := json.Marshal(o) 90 | require.NoError(t, err) 91 | require.JSONEq(t, `{"old":null,"new":null}`, string(body)) 92 | }) 93 | t.Run("id", func(t *testing.T) { 94 | o := FakeRequest{Old: int64(1), New: IDOrName{ID: 1}} 95 | body, err := json.Marshal(o) 96 | require.NoError(t, err) 97 | require.JSONEq(t, `{"old":1,"new":1}`, string(body)) 98 | }) 99 | t.Run("name", func(t *testing.T) { 100 | o := FakeRequest{Old: "name", New: IDOrName{Name: "name"}} 101 | body, err := json.Marshal(o) 102 | require.NoError(t, err) 103 | require.JSONEq(t, `{"old":"name","new":"name"}`, string(body)) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /hcloud/schema/image.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // Image defines the schema of an image. 6 | type Image struct { 7 | ID int64 `json:"id"` 8 | Status string `json:"status"` 9 | Type string `json:"type"` 10 | Name *string `json:"name"` 11 | Description string `json:"description"` 12 | ImageSize *float32 `json:"image_size"` 13 | DiskSize float32 `json:"disk_size"` 14 | Created *time.Time `json:"created"` 15 | CreatedFrom *ImageCreatedFrom `json:"created_from"` 16 | BoundTo *int64 `json:"bound_to"` 17 | OSFlavor string `json:"os_flavor"` 18 | OSVersion *string `json:"os_version"` 19 | Architecture string `json:"architecture"` 20 | RapidDeploy bool `json:"rapid_deploy"` 21 | Protection ImageProtection `json:"protection"` 22 | Deprecated *time.Time `json:"deprecated"` 23 | Deleted *time.Time `json:"deleted"` 24 | Labels map[string]string `json:"labels"` 25 | } 26 | 27 | // ImageProtection represents the protection level of a image. 28 | type ImageProtection struct { 29 | Delete bool `json:"delete"` 30 | } 31 | 32 | // ImageCreatedFrom defines the schema of the images created from reference. 33 | type ImageCreatedFrom struct { 34 | ID int64 `json:"id"` 35 | Name string `json:"name"` 36 | } 37 | 38 | // ImageGetResponse defines the schema of the response when 39 | // retrieving a single image. 40 | type ImageGetResponse struct { 41 | Image Image `json:"image"` 42 | } 43 | 44 | // ImageListResponse defines the schema of the response when 45 | // listing images. 46 | type ImageListResponse struct { 47 | Images []Image `json:"images"` 48 | } 49 | 50 | // ImageUpdateRequest defines the schema of the request to update an image. 51 | type ImageUpdateRequest struct { 52 | Description *string `json:"description,omitempty"` 53 | Type *string `json:"type,omitempty"` 54 | Labels *map[string]string `json:"labels,omitempty"` 55 | } 56 | 57 | // ImageUpdateResponse defines the schema of the response when updating an image. 58 | type ImageUpdateResponse struct { 59 | Image Image `json:"image"` 60 | } 61 | 62 | // ImageActionChangeProtectionRequest defines the schema of the request to change the resource protection of an image. 63 | type ImageActionChangeProtectionRequest struct { 64 | Delete *bool `json:"delete,omitempty"` 65 | } 66 | 67 | // ImageActionChangeProtectionResponse defines the schema of the response when changing the resource protection of an image. 68 | type ImageActionChangeProtectionResponse struct { 69 | Action Action `json:"action"` 70 | } 71 | -------------------------------------------------------------------------------- /hcloud/schema/image_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestImageUpdateRequest(t *testing.T) { 10 | var ( 11 | oneLabel = map[string]string{"foo": "bar"} 12 | nilLabels map[string]string 13 | emptyLabels = map[string]string{} 14 | ) 15 | 16 | testCases := []struct { 17 | name string 18 | in ImageUpdateRequest 19 | out []byte 20 | }{ 21 | { 22 | name: "no labels", 23 | in: ImageUpdateRequest{}, 24 | out: []byte(`{}`), 25 | }, 26 | { 27 | name: "one label", 28 | in: ImageUpdateRequest{Labels: &oneLabel}, 29 | out: []byte(`{"labels":{"foo":"bar"}}`), 30 | }, 31 | { 32 | name: "nil labels", 33 | in: ImageUpdateRequest{Labels: &nilLabels}, 34 | out: []byte(`{"labels":null}`), 35 | }, 36 | { 37 | name: "empty labels", 38 | in: ImageUpdateRequest{Labels: &emptyLabels}, 39 | out: []byte(`{"labels":{}}`), 40 | }, 41 | } 42 | 43 | for _, testCase := range testCases { 44 | t.Run(testCase.name, func(t *testing.T) { 45 | data, err := json.Marshal(testCase.in) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !bytes.Equal(data, testCase.out) { 50 | t.Fatalf("output %s does not match %s", data, testCase.out) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hcloud/schema/iso.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // ISO defines the schema of an ISO image. 4 | type ISO struct { 5 | ID int64 `json:"id"` 6 | Name string `json:"name"` 7 | Description string `json:"description"` 8 | Type string `json:"type"` 9 | Architecture *string `json:"architecture"` 10 | DeprecatableResource 11 | } 12 | 13 | // ISOGetResponse defines the schema of the response when retrieving a single ISO. 14 | type ISOGetResponse struct { 15 | ISO ISO `json:"iso"` 16 | } 17 | 18 | // ISOListResponse defines the schema of the response when listing ISOs. 19 | type ISOListResponse struct { 20 | ISOs []ISO `json:"isos"` 21 | } 22 | -------------------------------------------------------------------------------- /hcloud/schema/load_balancer_type.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // LoadBalancerType defines the schema of a LoadBalancer type. 4 | type LoadBalancerType struct { 5 | ID int64 `json:"id"` 6 | Name string `json:"name"` 7 | Description string `json:"description"` 8 | MaxConnections int `json:"max_connections"` 9 | MaxServices int `json:"max_services"` 10 | MaxTargets int `json:"max_targets"` 11 | MaxAssignedCertificates int `json:"max_assigned_certificates"` 12 | Prices []PricingLoadBalancerTypePrice `json:"prices"` 13 | Deprecated *string `json:"deprecated"` 14 | } 15 | 16 | // LoadBalancerTypeListResponse defines the schema of the response when 17 | // listing LoadBalancer types. 18 | type LoadBalancerTypeListResponse struct { 19 | LoadBalancerTypes []LoadBalancerType `json:"load_balancer_types"` 20 | } 21 | 22 | // LoadBalancerTypeGetResponse defines the schema of the response when 23 | // retrieving a single LoadBalancer type. 24 | type LoadBalancerTypeGetResponse struct { 25 | LoadBalancerType LoadBalancerType `json:"load_balancer_type"` 26 | } 27 | -------------------------------------------------------------------------------- /hcloud/schema/location.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // Location defines the schema of a location. 4 | type Location struct { 5 | ID int64 `json:"id"` 6 | Name string `json:"name"` 7 | Description string `json:"description"` 8 | Country string `json:"country"` 9 | City string `json:"city"` 10 | Latitude float64 `json:"latitude"` 11 | Longitude float64 `json:"longitude"` 12 | NetworkZone string `json:"network_zone"` 13 | } 14 | 15 | // LocationGetResponse defines the schema of the response when retrieving a single location. 16 | type LocationGetResponse struct { 17 | Location Location `json:"location"` 18 | } 19 | 20 | // LocationListResponse defines the schema of the response when listing locations. 21 | type LocationListResponse struct { 22 | Locations []Location `json:"locations"` 23 | } 24 | -------------------------------------------------------------------------------- /hcloud/schema/meta.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // Meta defines the schema of meta information which may be included 4 | // in responses. 5 | type Meta struct { 6 | Pagination *MetaPagination `json:"pagination"` 7 | } 8 | 9 | // MetaPagination defines the schema of pagination information. 10 | type MetaPagination struct { 11 | Page int `json:"page"` 12 | PerPage int `json:"per_page"` 13 | PreviousPage int `json:"previous_page"` 14 | NextPage int `json:"next_page"` 15 | LastPage int `json:"last_page"` 16 | TotalEntries int `json:"total_entries"` 17 | } 18 | 19 | // MetaResponse defines the schema of a response containing 20 | // meta information. 21 | type MetaResponse struct { 22 | Meta Meta `json:"meta"` 23 | } 24 | -------------------------------------------------------------------------------- /hcloud/schema/network_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestNetworkUpdateRequest(t *testing.T) { 10 | var ( 11 | oneLabel = map[string]string{"foo": "bar"} 12 | nilLabels map[string]string 13 | emptyLabels = map[string]string{} 14 | ) 15 | 16 | testCases := []struct { 17 | name string 18 | in NetworkUpdateRequest 19 | out []byte 20 | }{ 21 | { 22 | name: "no labels", 23 | in: NetworkUpdateRequest{}, 24 | out: []byte(`{}`), 25 | }, 26 | { 27 | name: "one label", 28 | in: NetworkUpdateRequest{Labels: &oneLabel}, 29 | out: []byte(`{"labels":{"foo":"bar"}}`), 30 | }, 31 | { 32 | name: "nil labels", 33 | in: NetworkUpdateRequest{Labels: &nilLabels}, 34 | out: []byte(`{"labels":null}`), 35 | }, 36 | { 37 | name: "empty labels", 38 | in: NetworkUpdateRequest{Labels: &emptyLabels}, 39 | out: []byte(`{"labels":{}}`), 40 | }, 41 | } 42 | 43 | for _, testCase := range testCases { 44 | t.Run(testCase.name, func(t *testing.T) { 45 | data, err := json.Marshal(testCase.in) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !bytes.Equal(data, testCase.out) { 50 | t.Fatalf("output %s does not match %s", data, testCase.out) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hcloud/schema/placement_group.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | type PlacementGroup struct { 6 | ID int64 `json:"id"` 7 | Name string `json:"name"` 8 | Labels map[string]string `json:"labels"` 9 | Created time.Time `json:"created"` 10 | Servers []int64 `json:"servers"` 11 | Type string `json:"type"` 12 | } 13 | 14 | type PlacementGroupListResponse struct { 15 | PlacementGroups []PlacementGroup `json:"placement_groups"` 16 | } 17 | 18 | type PlacementGroupGetResponse struct { 19 | PlacementGroup PlacementGroup `json:"placement_group"` 20 | } 21 | 22 | type PlacementGroupCreateRequest struct { 23 | Name string `json:"name"` 24 | Labels *map[string]string `json:"labels,omitempty"` 25 | Type string `json:"type"` 26 | } 27 | 28 | type PlacementGroupCreateResponse struct { 29 | PlacementGroup PlacementGroup `json:"placement_group"` 30 | Action *Action `json:"action"` 31 | } 32 | 33 | type PlacementGroupUpdateRequest struct { 34 | Name *string `json:"name,omitempty"` 35 | Labels *map[string]string `json:"labels,omitempty"` 36 | } 37 | 38 | type PlacementGroupUpdateResponse struct { 39 | PlacementGroup PlacementGroup `json:"placement_group"` 40 | } 41 | -------------------------------------------------------------------------------- /hcloud/schema/pricing.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // Pricing defines the schema for pricing information. 4 | type Pricing struct { 5 | Currency string `json:"currency"` 6 | VATRate string `json:"vat_rate"` 7 | Image PricingImage `json:"image"` 8 | // Deprecated: [Pricing.FloatingIP] is deprecated, use [Pricing.FloatingIPs] instead. 9 | FloatingIP PricingFloatingIP `json:"floating_ip"` 10 | FloatingIPs []PricingFloatingIPType `json:"floating_ips"` 11 | PrimaryIPs []PricingPrimaryIP `json:"primary_ips"` 12 | // Deprecated: [Pricing.Traffic] is deprecated and will report 0 after 2024-08-05. 13 | // Use traffic pricing from [Pricing.ServerTypes] or [Pricing.LoadBalancerTypes] instead. 14 | Traffic PricingTraffic `json:"traffic"` 15 | ServerBackup PricingServerBackup `json:"server_backup"` 16 | ServerTypes []PricingServerType `json:"server_types"` 17 | LoadBalancerTypes []PricingLoadBalancerType `json:"load_balancer_types"` 18 | Volume PricingVolume `json:"volume"` 19 | } 20 | 21 | // Price defines the schema of a single price with net and gross amount. 22 | type Price struct { 23 | Net string `json:"net"` 24 | Gross string `json:"gross"` 25 | } 26 | 27 | // PricingImage defines the schema of pricing information for an image. 28 | type PricingImage struct { 29 | PricePerGBMonth Price `json:"price_per_gb_month"` 30 | } 31 | 32 | // PricingFloatingIP defines the schema of pricing information for a Floating IP. 33 | type PricingFloatingIP struct { 34 | PriceMonthly Price `json:"price_monthly"` 35 | } 36 | 37 | // PricingFloatingIPType defines the schema of pricing information for a Floating IP per type. 38 | type PricingFloatingIPType struct { 39 | Type string `json:"type"` 40 | Prices []PricingFloatingIPTypePrice `json:"prices"` 41 | } 42 | 43 | // PricingFloatingIPTypePrice defines the schema of pricing information for a Floating IP 44 | // type at a location. 45 | type PricingFloatingIPTypePrice struct { 46 | Location string `json:"location"` 47 | PriceMonthly Price `json:"price_monthly"` 48 | } 49 | 50 | // PricingTraffic defines the schema of pricing information for traffic. 51 | type PricingTraffic struct { 52 | PricePerTB Price `json:"price_per_tb"` 53 | } 54 | 55 | // PricingVolume defines the schema of pricing information for a Volume. 56 | type PricingVolume struct { 57 | PricePerGBPerMonth Price `json:"price_per_gb_month"` 58 | } 59 | 60 | // PricingServerBackup defines the schema of pricing information for server backups. 61 | type PricingServerBackup struct { 62 | Percentage string `json:"percentage"` 63 | } 64 | 65 | // PricingServerType defines the schema of pricing information for a server type. 66 | type PricingServerType struct { 67 | ID int64 `json:"id"` 68 | Name string `json:"name"` 69 | Prices []PricingServerTypePrice `json:"prices"` 70 | } 71 | 72 | // PricingServerTypePrice defines the schema of pricing information for a server 73 | // type at a location. 74 | type PricingServerTypePrice struct { 75 | Location string `json:"location"` 76 | PriceHourly Price `json:"price_hourly"` 77 | PriceMonthly Price `json:"price_monthly"` 78 | 79 | IncludedTraffic uint64 `json:"included_traffic"` 80 | PricePerTBTraffic Price `json:"price_per_tb_traffic"` 81 | } 82 | 83 | // PricingLoadBalancerType defines the schema of pricing information for a Load Balancer type. 84 | type PricingLoadBalancerType struct { 85 | ID int64 `json:"id"` 86 | Name string `json:"name"` 87 | Prices []PricingLoadBalancerTypePrice `json:"prices"` 88 | } 89 | 90 | // PricingLoadBalancerTypePrice defines the schema of pricing information for a Load Balancer 91 | // type at a location. 92 | type PricingLoadBalancerTypePrice struct { 93 | Location string `json:"location"` 94 | PriceHourly Price `json:"price_hourly"` 95 | PriceMonthly Price `json:"price_monthly"` 96 | 97 | IncludedTraffic uint64 `json:"included_traffic"` 98 | PricePerTBTraffic Price `json:"price_per_tb_traffic"` 99 | } 100 | 101 | // PricingGetResponse defines the schema of the response when retrieving pricing information. 102 | type PricingGetResponse struct { 103 | Pricing Pricing `json:"pricing"` 104 | } 105 | 106 | // PricingPrimaryIPTypePrice defines the schema of pricing information for a primary IP. 107 | // type at a datacenter. 108 | type PricingPrimaryIPTypePrice struct { 109 | Datacenter string `json:"datacenter"` // Deprecated: the API does not return pricing for the individual DCs anymore 110 | Location string `json:"location"` 111 | PriceHourly Price `json:"price_hourly"` 112 | PriceMonthly Price `json:"price_monthly"` 113 | } 114 | 115 | // PricingPrimaryIP define the schema of pricing information for a primary IP at a datacenter. 116 | type PricingPrimaryIP struct { 117 | Type string `json:"type"` 118 | Prices []PricingPrimaryIPTypePrice `json:"prices"` 119 | } 120 | -------------------------------------------------------------------------------- /hcloud/schema/primary_ip.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // PrimaryIP defines a Primary IP. 6 | type PrimaryIP struct { 7 | ID int64 `json:"id"` 8 | IP string `json:"ip"` 9 | Labels map[string]string `json:"labels"` 10 | Name string `json:"name"` 11 | Type string `json:"type"` 12 | Protection PrimaryIPProtection `json:"protection"` 13 | DNSPtr []PrimaryIPDNSPTR `json:"dns_ptr"` 14 | AssigneeID *int64 `json:"assignee_id"` 15 | AssigneeType string `json:"assignee_type"` 16 | AutoDelete bool `json:"auto_delete"` 17 | Blocked bool `json:"blocked"` 18 | Created time.Time `json:"created"` 19 | Datacenter Datacenter `json:"datacenter"` 20 | } 21 | 22 | // PrimaryIPProtection represents the protection level of a Primary IP. 23 | type PrimaryIPProtection struct { 24 | Delete bool `json:"delete"` 25 | } 26 | 27 | // PrimaryIPDNSPTR contains reverse DNS information for a 28 | // IPv4 or IPv6 Primary IP. 29 | type PrimaryIPDNSPTR struct { 30 | DNSPtr string `json:"dns_ptr"` 31 | IP string `json:"ip"` 32 | } 33 | 34 | // PrimaryIPCreateOpts defines the request to 35 | // create a Primary IP. 36 | type PrimaryIPCreateRequest struct { 37 | Name string `json:"name"` 38 | Type string `json:"type"` 39 | AssigneeType string `json:"assignee_type"` 40 | AssigneeID *int64 `json:"assignee_id,omitempty"` 41 | Labels map[string]string `json:"labels,omitempty"` 42 | AutoDelete *bool `json:"auto_delete,omitempty"` 43 | Datacenter string `json:"datacenter,omitempty"` 44 | } 45 | 46 | // PrimaryIPCreateResponse defines the schema of the response 47 | // when creating a Primary IP. 48 | type PrimaryIPCreateResponse struct { 49 | PrimaryIP PrimaryIP `json:"primary_ip"` 50 | Action *Action `json:"action"` 51 | } 52 | 53 | // PrimaryIPGetResponse defines the response when retrieving a single Primary IP. 54 | type PrimaryIPGetResponse struct { 55 | PrimaryIP PrimaryIP `json:"primary_ip"` 56 | } 57 | 58 | // PrimaryIPListResponse defines the response when listing Primary IPs. 59 | type PrimaryIPListResponse struct { 60 | PrimaryIPs []PrimaryIP `json:"primary_ips"` 61 | } 62 | 63 | // PrimaryIPUpdateOpts defines the request to 64 | // update a Primary IP. 65 | type PrimaryIPUpdateRequest struct { 66 | Name string `json:"name,omitempty"` 67 | Labels map[string]string `json:"labels,omitempty"` 68 | AutoDelete *bool `json:"auto_delete,omitempty"` 69 | } 70 | 71 | // PrimaryIPUpdateResponse defines the response 72 | // when updating a Primary IP. 73 | type PrimaryIPUpdateResponse struct { 74 | PrimaryIP PrimaryIP `json:"primary_ip"` 75 | } 76 | 77 | // PrimaryIPActionChangeDNSPtrRequest defines the schema for the request to 78 | // change a Primary IP's reverse DNS pointer. 79 | type PrimaryIPActionChangeDNSPtrRequest struct { 80 | IP string `json:"ip"` 81 | DNSPtr *string `json:"dns_ptr"` 82 | } 83 | 84 | // PrimaryIPActionChangeDNSPtrResponse defines the response when setting a reverse DNS 85 | // pointer for a IP address. 86 | type PrimaryIPActionChangeDNSPtrResponse struct { 87 | Action Action `json:"action"` 88 | } 89 | 90 | // PrimaryIPActionAssignRequest defines the request to 91 | // assign a Primary IP to an assignee (usually a server). 92 | type PrimaryIPActionAssignRequest struct { 93 | AssigneeID int64 `json:"assignee_id"` 94 | AssigneeType string `json:"assignee_type"` 95 | } 96 | 97 | // PrimaryIPActionAssignResponse defines the response when assigning a Primary IP to a 98 | // assignee. 99 | type PrimaryIPActionAssignResponse struct { 100 | Action Action `json:"action"` 101 | } 102 | 103 | // PrimaryIPActionUnassignResponse defines the response to unassign a Primary IP. 104 | type PrimaryIPActionUnassignResponse struct { 105 | Action Action `json:"action"` 106 | } 107 | 108 | // PrimaryIPActionChangeProtectionRequest defines the request to 109 | // change protection configuration of a Primary IP. 110 | type PrimaryIPActionChangeProtectionRequest struct { 111 | Delete bool `json:"delete"` 112 | } 113 | 114 | // PrimaryIPActionChangeProtectionResponse defines the response when changing the 115 | // protection of a Primary IP. 116 | type PrimaryIPActionChangeProtectionResponse struct { 117 | Action Action `json:"action"` 118 | } 119 | -------------------------------------------------------------------------------- /hcloud/schema/server_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestServerActionCreateImageRequest(t *testing.T) { 10 | var ( 11 | oneLabel = map[string]string{"foo": "bar"} 12 | nilLabels map[string]string 13 | emptyLabels = map[string]string{} 14 | ) 15 | 16 | testCases := []struct { 17 | name string 18 | in ServerActionCreateImageRequest 19 | out []byte 20 | }{ 21 | { 22 | name: "no labels", 23 | in: ServerActionCreateImageRequest{}, 24 | out: []byte(`{"type":null,"description":null}`), 25 | }, 26 | { 27 | name: "one label", 28 | in: ServerActionCreateImageRequest{Labels: &oneLabel}, 29 | out: []byte(`{"type":null,"description":null,"labels":{"foo":"bar"}}`), 30 | }, 31 | { 32 | name: "nil labels", 33 | in: ServerActionCreateImageRequest{Labels: &nilLabels}, 34 | out: []byte(`{"type":null,"description":null,"labels":null}`), 35 | }, 36 | { 37 | name: "empty labels", 38 | in: ServerActionCreateImageRequest{Labels: &emptyLabels}, 39 | out: []byte(`{"type":null,"description":null,"labels":{}}`), 40 | }, 41 | } 42 | 43 | for _, testCase := range testCases { 44 | t.Run(testCase.name, func(t *testing.T) { 45 | data, err := json.Marshal(testCase.in) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !bytes.Equal(data, testCase.out) { 50 | t.Fatalf("output %s does not match %s", data, testCase.out) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hcloud/schema/server_type.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | // ServerType defines the schema of a server type. 4 | type ServerType struct { 5 | ID int64 `json:"id"` 6 | Name string `json:"name"` 7 | Description string `json:"description"` 8 | Cores int `json:"cores"` 9 | Memory float32 `json:"memory"` 10 | Disk int `json:"disk"` 11 | StorageType string `json:"storage_type"` 12 | CPUType string `json:"cpu_type"` 13 | Architecture string `json:"architecture"` 14 | 15 | // Deprecated: [ServerType.IncludedTraffic] is deprecated and will always report 0 after 2024-08-05. 16 | // Use [ServerType.Prices] instead to get the included traffic for each location. 17 | IncludedTraffic int64 `json:"included_traffic"` 18 | Prices []PricingServerTypePrice `json:"prices"` 19 | Deprecated bool `json:"deprecated"` 20 | DeprecatableResource 21 | } 22 | 23 | // ServerTypeListResponse defines the schema of the response when 24 | // listing server types. 25 | type ServerTypeListResponse struct { 26 | ServerTypes []ServerType `json:"server_types"` 27 | } 28 | 29 | // ServerTypeGetResponse defines the schema of the response when 30 | // retrieving a single server type. 31 | type ServerTypeGetResponse struct { 32 | ServerType ServerType `json:"server_type"` 33 | } 34 | -------------------------------------------------------------------------------- /hcloud/schema/ssh_key.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // SSHKey defines the schema of a SSH key. 6 | type SSHKey struct { 7 | ID int64 `json:"id"` 8 | Name string `json:"name"` 9 | Fingerprint string `json:"fingerprint"` 10 | PublicKey string `json:"public_key"` 11 | Labels map[string]string `json:"labels"` 12 | Created time.Time `json:"created"` 13 | } 14 | 15 | // SSHKeyCreateRequest defines the schema of the request 16 | // to create a SSH key. 17 | type SSHKeyCreateRequest struct { 18 | Name string `json:"name"` 19 | PublicKey string `json:"public_key"` 20 | Labels *map[string]string `json:"labels,omitempty"` 21 | } 22 | 23 | // SSHKeyCreateResponse defines the schema of the response 24 | // when creating a SSH key. 25 | type SSHKeyCreateResponse struct { 26 | SSHKey SSHKey `json:"ssh_key"` 27 | } 28 | 29 | // SSHKeyListResponse defines the schema of the response 30 | // when listing SSH keys. 31 | type SSHKeyListResponse struct { 32 | SSHKeys []SSHKey `json:"ssh_keys"` 33 | } 34 | 35 | // SSHKeyGetResponse defines the schema of the response 36 | // when retrieving a single SSH key. 37 | type SSHKeyGetResponse struct { 38 | SSHKey SSHKey `json:"ssh_key"` 39 | } 40 | 41 | // SSHKeyUpdateRequest defines the schema of the request to update a SSH key. 42 | type SSHKeyUpdateRequest struct { 43 | Name string `json:"name,omitempty"` 44 | Labels *map[string]string `json:"labels,omitempty"` 45 | } 46 | 47 | // SSHKeyUpdateResponse defines the schema of the response when updating a SSH key. 48 | type SSHKeyUpdateResponse struct { 49 | SSHKey SSHKey `json:"ssh_key"` 50 | } 51 | -------------------------------------------------------------------------------- /hcloud/schema/ssh_key_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestSSHKeyCreateRequest(t *testing.T) { 10 | var ( 11 | oneLabel = map[string]string{"foo": "bar"} 12 | nilLabels map[string]string 13 | emptyLabels = map[string]string{} 14 | ) 15 | 16 | testCases := []struct { 17 | name string 18 | in SSHKeyCreateRequest 19 | out []byte 20 | }{ 21 | { 22 | name: "no labels", 23 | in: SSHKeyCreateRequest{Name: "test", PublicKey: "key"}, 24 | out: []byte(`{"name":"test","public_key":"key"}`), 25 | }, 26 | { 27 | name: "one label", 28 | in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &oneLabel}, 29 | out: []byte(`{"name":"test","public_key":"key","labels":{"foo":"bar"}}`), 30 | }, 31 | { 32 | name: "nil labels", 33 | in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &nilLabels}, 34 | out: []byte(`{"name":"test","public_key":"key","labels":null}`), 35 | }, 36 | { 37 | name: "empty labels", 38 | in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &emptyLabels}, 39 | out: []byte(`{"name":"test","public_key":"key","labels":{}}`), 40 | }, 41 | } 42 | 43 | for _, testCase := range testCases { 44 | t.Run(testCase.name, func(t *testing.T) { 45 | data, err := json.Marshal(testCase.in) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !bytes.Equal(data, testCase.out) { 50 | t.Fatalf("output %s does not match %s", data, testCase.out) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hcloud/schema/volume.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | // Volume defines the schema of a volume. 6 | type Volume struct { 7 | ID int64 `json:"id"` 8 | Name string `json:"name"` 9 | Server *int64 `json:"server"` 10 | Status string `json:"status"` 11 | Location Location `json:"location"` 12 | Size int `json:"size"` 13 | Format *string `json:"format"` 14 | Protection VolumeProtection `json:"protection"` 15 | Labels map[string]string `json:"labels"` 16 | LinuxDevice string `json:"linux_device"` 17 | Created time.Time `json:"created"` 18 | } 19 | 20 | // VolumeCreateRequest defines the schema of the request 21 | // to create a volume. 22 | type VolumeCreateRequest struct { 23 | Name string `json:"name"` 24 | Size int `json:"size"` 25 | Server *int64 `json:"server,omitempty"` 26 | Location *IDOrName `json:"location,omitempty"` 27 | Labels *map[string]string `json:"labels,omitempty"` 28 | Automount *bool `json:"automount,omitempty"` 29 | Format *string `json:"format,omitempty"` 30 | } 31 | 32 | // VolumeCreateResponse defines the schema of the response 33 | // when creating a volume. 34 | type VolumeCreateResponse struct { 35 | Volume Volume `json:"volume"` 36 | Action *Action `json:"action"` 37 | NextActions []Action `json:"next_actions"` 38 | } 39 | 40 | // VolumeListResponse defines the schema of the response 41 | // when listing volumes. 42 | type VolumeListResponse struct { 43 | Volumes []Volume `json:"volumes"` 44 | } 45 | 46 | // VolumeGetResponse defines the schema of the response 47 | // when retrieving a single volume. 48 | type VolumeGetResponse struct { 49 | Volume Volume `json:"volume"` 50 | } 51 | 52 | // VolumeUpdateRequest defines the schema of the request to update a volume. 53 | type VolumeUpdateRequest struct { 54 | Name string `json:"name,omitempty"` 55 | Labels *map[string]string `json:"labels,omitempty"` 56 | } 57 | 58 | // VolumeUpdateResponse defines the schema of the response when updating a volume. 59 | type VolumeUpdateResponse struct { 60 | Volume Volume `json:"volume"` 61 | } 62 | 63 | // VolumeProtection defines the schema of a volume's resource protection. 64 | type VolumeProtection struct { 65 | Delete bool `json:"delete"` 66 | } 67 | 68 | // VolumeActionChangeProtectionRequest defines the schema of the request to 69 | // change the resource protection of a volume. 70 | type VolumeActionChangeProtectionRequest struct { 71 | Delete *bool `json:"delete,omitempty"` 72 | } 73 | 74 | // VolumeActionChangeProtectionResponse defines the schema of the response when 75 | // changing the resource protection of a volume. 76 | type VolumeActionChangeProtectionResponse struct { 77 | Action Action `json:"action"` 78 | } 79 | 80 | // VolumeActionAttachVolumeRequest defines the schema of the request to 81 | // attach a volume to a server. 82 | type VolumeActionAttachVolumeRequest struct { 83 | Server int64 `json:"server"` 84 | Automount *bool `json:"automount,omitempty"` 85 | } 86 | 87 | // VolumeActionAttachVolumeResponse defines the schema of the response when 88 | // attaching a volume to a server. 89 | type VolumeActionAttachVolumeResponse struct { 90 | Action Action `json:"action"` 91 | } 92 | 93 | // VolumeActionDetachVolumeRequest defines the schema of the request to 94 | // create an detach volume action. 95 | type VolumeActionDetachVolumeRequest struct{} 96 | 97 | // VolumeActionDetachVolumeResponse defines the schema of the response when 98 | // creating an detach volume action. 99 | type VolumeActionDetachVolumeResponse struct { 100 | Action Action `json:"action"` 101 | } 102 | 103 | // VolumeActionResizeVolumeRequest defines the schema of the request to resize a volume. 104 | type VolumeActionResizeVolumeRequest struct { 105 | Size int `json:"size"` 106 | } 107 | 108 | // VolumeActionResizeVolumeResponse defines the schema of the response when resizing a volume. 109 | type VolumeActionResizeVolumeResponse struct { 110 | Action Action `json:"action"` 111 | } 112 | -------------------------------------------------------------------------------- /hcloud/schema/volume_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestVolumeUpdateRequest(t *testing.T) { 10 | var ( 11 | oneLabel = map[string]string{"foo": "bar"} 12 | nilLabels map[string]string 13 | emptyLabels = map[string]string{} 14 | ) 15 | 16 | testCases := []struct { 17 | name string 18 | in VolumeUpdateRequest 19 | out []byte 20 | }{ 21 | { 22 | name: "no labels", 23 | in: VolumeUpdateRequest{}, 24 | out: []byte(`{}`), 25 | }, 26 | { 27 | name: "one label", 28 | in: VolumeUpdateRequest{Labels: &oneLabel}, 29 | out: []byte(`{"labels":{"foo":"bar"}}`), 30 | }, 31 | { 32 | name: "nil labels", 33 | in: VolumeUpdateRequest{Labels: &nilLabels}, 34 | out: []byte(`{"labels":null}`), 35 | }, 36 | { 37 | name: "empty labels", 38 | in: VolumeUpdateRequest{Labels: &emptyLabels}, 39 | out: []byte(`{"labels":{}}`), 40 | }, 41 | } 42 | 43 | for _, testCase := range testCases { 44 | t.Run(testCase.name, func(t *testing.T) { 45 | data, err := json.Marshal(testCase.in) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !bytes.Equal(data, testCase.out) { 50 | t.Fatalf("output %s does not match %s", data, testCase.out) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hcloud/schema_assign.go: -------------------------------------------------------------------------------- 1 | //go:build !goverter 2 | 3 | package hcloud 4 | 5 | /* 6 | This file is needed so that c is assigned to a converterImpl{}. 7 | If we did this in schema.go, goverter would fail because of a 8 | compiler error (converterImpl might not be defined). 9 | Because this file is not compiled by goverter, we can safely 10 | assign c here. 11 | */ 12 | 13 | func init() { 14 | c = &converterImpl{} 15 | } 16 | -------------------------------------------------------------------------------- /hcloud/server_type.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | 9 | "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" 10 | "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" 11 | ) 12 | 13 | // ServerType represents a server type in the Hetzner Cloud. 14 | type ServerType struct { 15 | ID int64 16 | Name string 17 | Description string 18 | Cores int 19 | Memory float32 20 | Disk int 21 | StorageType StorageType 22 | CPUType CPUType 23 | Architecture Architecture 24 | 25 | // Deprecated: [ServerType.IncludedTraffic] is deprecated and will always report 0 after 2024-08-05. 26 | // Use [ServerType.Pricings] instead to get the included traffic for each location. 27 | IncludedTraffic int64 28 | Pricings []ServerTypeLocationPricing 29 | DeprecatableResource 30 | } 31 | 32 | // StorageType specifies the type of storage. 33 | type StorageType string 34 | 35 | const ( 36 | // StorageTypeLocal is the type for local storage. 37 | StorageTypeLocal StorageType = "local" 38 | 39 | // StorageTypeCeph is the type for remote storage. 40 | StorageTypeCeph StorageType = "ceph" 41 | ) 42 | 43 | // CPUType specifies the type of the CPU. 44 | type CPUType string 45 | 46 | const ( 47 | // CPUTypeShared is the type for shared CPU. 48 | CPUTypeShared CPUType = "shared" 49 | 50 | // CPUTypeDedicated is the type for dedicated CPU. 51 | CPUTypeDedicated CPUType = "dedicated" 52 | ) 53 | 54 | // ServerTypeClient is a client for the server types API. 55 | type ServerTypeClient struct { 56 | client *Client 57 | } 58 | 59 | // GetByID retrieves a server type by its ID. If the server type does not exist, nil is returned. 60 | func (c *ServerTypeClient) GetByID(ctx context.Context, id int64) (*ServerType, *Response, error) { 61 | const opPath = "/server_types/%d" 62 | ctx = ctxutil.SetOpPath(ctx, opPath) 63 | 64 | reqPath := fmt.Sprintf(opPath, id) 65 | 66 | respBody, resp, err := getRequest[schema.ServerTypeGetResponse](ctx, c.client, reqPath) 67 | if err != nil { 68 | if IsError(err, ErrorCodeNotFound) { 69 | return nil, resp, nil 70 | } 71 | return nil, resp, err 72 | } 73 | 74 | return ServerTypeFromSchema(respBody.ServerType), resp, nil 75 | } 76 | 77 | // GetByName retrieves a server type by its name. If the server type does not exist, nil is returned. 78 | func (c *ServerTypeClient) GetByName(ctx context.Context, name string) (*ServerType, *Response, error) { 79 | return firstByName(name, func() ([]*ServerType, *Response, error) { 80 | return c.List(ctx, ServerTypeListOpts{Name: name}) 81 | }) 82 | } 83 | 84 | // Get retrieves a server type by its ID if the input can be parsed as an integer, otherwise it 85 | // retrieves a server type by its name. If the server type does not exist, nil is returned. 86 | func (c *ServerTypeClient) Get(ctx context.Context, idOrName string) (*ServerType, *Response, error) { 87 | if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil { 88 | return c.GetByID(ctx, id) 89 | } 90 | return c.GetByName(ctx, idOrName) 91 | } 92 | 93 | // ServerTypeListOpts specifies options for listing server types. 94 | type ServerTypeListOpts struct { 95 | ListOpts 96 | Name string 97 | Sort []string 98 | } 99 | 100 | func (l ServerTypeListOpts) values() url.Values { 101 | vals := l.ListOpts.Values() 102 | if l.Name != "" { 103 | vals.Add("name", l.Name) 104 | } 105 | for _, sort := range l.Sort { 106 | vals.Add("sort", sort) 107 | } 108 | return vals 109 | } 110 | 111 | // List returns a list of server types for a specific page. 112 | // 113 | // Please note that filters specified in opts are not taken into account 114 | // when their value corresponds to their zero value or when they are empty. 115 | func (c *ServerTypeClient) List(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, *Response, error) { 116 | const opPath = "/server_types?%s" 117 | ctx = ctxutil.SetOpPath(ctx, opPath) 118 | 119 | reqPath := fmt.Sprintf(opPath, opts.values().Encode()) 120 | 121 | respBody, resp, err := getRequest[schema.ServerTypeListResponse](ctx, c.client, reqPath) 122 | if err != nil { 123 | return nil, resp, err 124 | } 125 | 126 | return allFromSchemaFunc(respBody.ServerTypes, ServerTypeFromSchema), resp, nil 127 | } 128 | 129 | // All returns all server types. 130 | func (c *ServerTypeClient) All(ctx context.Context) ([]*ServerType, error) { 131 | return c.AllWithOpts(ctx, ServerTypeListOpts{ListOpts: ListOpts{PerPage: 50}}) 132 | } 133 | 134 | // AllWithOpts returns all server types for the given options. 135 | func (c *ServerTypeClient) AllWithOpts(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, error) { 136 | return iterPages(func(page int) ([]*ServerType, *Response, error) { 137 | opts.Page = page 138 | return c.List(ctx, opts) 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /hcloud/testing.go: -------------------------------------------------------------------------------- 1 | package hcloud 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func mustParseTime(t *testing.T, value string) time.Time { 9 | t.Helper() 10 | 11 | ts, err := time.Parse(time.RFC3339, value) 12 | if err != nil { 13 | t.Fatalf("parse time: value %v: %v", value, err) 14 | } 15 | return ts 16 | } 17 | -------------------------------------------------------------------------------- /hcloud/zz_action_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IActionClient ... 10 | type IActionClient interface { 11 | // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Action, *Response, error) 13 | // List returns a list of actions for a specific page. 14 | // 15 | // Please note that filters specified in opts are not taken into account 16 | // when their value corresponds to their zero value or when they are empty. 17 | List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) 18 | // All returns all actions. 19 | // 20 | // Deprecated: It is required to pass in a list of IDs since 30 January 2025. Please use [ActionClient.AllWithOpts] instead. 21 | All(ctx context.Context) ([]*Action, error) 22 | // AllWithOpts returns all actions for the given options. 23 | // 24 | // It is required to set [ActionListOpts.ID]. Any other fields set in the opts are ignored. 25 | AllWithOpts(ctx context.Context, opts ActionListOpts) ([]*Action, error) 26 | // WatchOverallProgress watches several actions' progress until they complete 27 | // with success or error. This watching happens in a goroutine and updates are 28 | // provided through the two returned channels: 29 | // 30 | // - The first channel receives percentage updates of the progress, based on 31 | // the number of completed versus total watched actions. The return value 32 | // is an int between 0 and 100. 33 | // - The second channel returned receives errors for actions that did not 34 | // complete successfully, as well as any errors that happened while 35 | // querying the API. 36 | // 37 | // By default, the method keeps watching until all actions have finished 38 | // processing. If you want to be able to cancel the method or configure a 39 | // timeout, use the [context.Context]. Once the method has stopped watching, 40 | // both returned channels are closed. 41 | // 42 | // WatchOverallProgress uses the [WithPollOpts] of the [Client] to wait 43 | // until sending the next request. 44 | // 45 | // Deprecated: WatchOverallProgress is deprecated, use [WaitForFunc] instead. 46 | WatchOverallProgress(ctx context.Context, actions []*Action) (<-chan int, <-chan error) 47 | // WatchProgress watches one action's progress until it completes with success 48 | // or error. This watching happens in a goroutine and updates are provided 49 | // through the two returned channels: 50 | // 51 | // - The first channel receives percentage updates of the progress, based on 52 | // the progress percentage indicated by the API. The return value is an int 53 | // between 0 and 100. 54 | // - The second channel receives any errors that happened while querying the 55 | // API, as well as the error of the action if it did not complete 56 | // successfully, or nil if it did. 57 | // 58 | // By default, the method keeps watching until the action has finished 59 | // processing. If you want to be able to cancel the method or configure a 60 | // timeout, use the [context.Context]. Once the method has stopped watching, 61 | // both returned channels are closed. 62 | // 63 | // WatchProgress uses the [WithPollOpts] of the [Client] to wait until 64 | // sending the next request. 65 | // 66 | // Deprecated: WatchProgress is deprecated, use [WaitForFunc] instead. 67 | WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) 68 | // WaitForFunc waits until all actions are completed by polling the API at the interval 69 | // defined by [WithPollOpts]. An action is considered as complete when its status is 70 | // either [ActionStatusSuccess] or [ActionStatusError]. 71 | // 72 | // The handleUpdate callback is called every time an action is updated. 73 | WaitForFunc(ctx context.Context, handleUpdate func(update *Action) error, actions ...*Action) error 74 | // WaitFor waits until all actions succeed by polling the API at the interval defined by 75 | // [WithPollOpts]. An action is considered as succeeded when its status is either 76 | // [ActionStatusSuccess]. 77 | // 78 | // If a single action fails, the function will stop waiting and the error set in the 79 | // action will be returned as an [ActionError]. 80 | // 81 | // For more flexibility, see the [ActionClient.WaitForFunc] function. 82 | WaitFor(ctx context.Context, actions ...*Action) error 83 | } 84 | -------------------------------------------------------------------------------- /hcloud/zz_certificate_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ICertificateClient ... 10 | type ICertificateClient interface { 11 | // GetByID retrieves a Certificate by its ID. If the Certificate does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Certificate, *Response, error) 13 | // GetByName retrieves a Certificate by its name. If the Certificate does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*Certificate, *Response, error) 15 | // Get retrieves a Certificate by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a Certificate by its name. If the Certificate does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*Certificate, *Response, error) 18 | // List returns a list of Certificates for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts CertificateListOpts) ([]*Certificate, *Response, error) 23 | // All returns all Certificates. 24 | All(ctx context.Context) ([]*Certificate, error) 25 | // AllWithOpts returns all Certificates for the given options. 26 | AllWithOpts(ctx context.Context, opts CertificateListOpts) ([]*Certificate, error) 27 | // Create creates a new uploaded certificate. 28 | // 29 | // Create returns an error for certificates of any other type. Use 30 | // CreateCertificate to create such certificates. 31 | Create(ctx context.Context, opts CertificateCreateOpts) (*Certificate, *Response, error) 32 | // CreateCertificate creates a new certificate of any type. 33 | CreateCertificate(ctx context.Context, opts CertificateCreateOpts) (CertificateCreateResult, *Response, error) 34 | // Update updates a Certificate. 35 | Update(ctx context.Context, certificate *Certificate, opts CertificateUpdateOpts) (*Certificate, *Response, error) 36 | // Delete deletes a certificate. 37 | Delete(ctx context.Context, certificate *Certificate) (*Response, error) 38 | // RetryIssuance retries the issuance of a failed managed certificate. 39 | RetryIssuance(ctx context.Context, certificate *Certificate) (*Action, *Response, error) 40 | } 41 | -------------------------------------------------------------------------------- /hcloud/zz_datacenter_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IDatacenterClient ... 10 | type IDatacenterClient interface { 11 | // GetByID retrieves a datacenter by its ID. If the datacenter does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Datacenter, *Response, error) 13 | // GetByName retrieves a datacenter by its name. If the datacenter does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) 15 | // Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a datacenter by its name. If the datacenter does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) 18 | // List returns a list of datacenters for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) 23 | // All returns all datacenters. 24 | All(ctx context.Context) ([]*Datacenter, error) 25 | // AllWithOpts returns all datacenters for the given options. 26 | AllWithOpts(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, error) 27 | } 28 | -------------------------------------------------------------------------------- /hcloud/zz_firewall_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IFirewallClient ... 10 | type IFirewallClient interface { 11 | // GetByID retrieves a Firewall by its ID. If the Firewall does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Firewall, *Response, error) 13 | // GetByName retrieves a Firewall by its name. If the Firewall does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*Firewall, *Response, error) 15 | // Get retrieves a Firewall by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a Firewall by its name. If the Firewall does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*Firewall, *Response, error) 18 | // List returns a list of Firewalls for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts FirewallListOpts) ([]*Firewall, *Response, error) 23 | // All returns all Firewalls. 24 | All(ctx context.Context) ([]*Firewall, error) 25 | // AllWithOpts returns all Firewalls for the given options. 26 | AllWithOpts(ctx context.Context, opts FirewallListOpts) ([]*Firewall, error) 27 | // Create creates a new Firewall. 28 | Create(ctx context.Context, opts FirewallCreateOpts) (FirewallCreateResult, *Response, error) 29 | // Update updates a Firewall. 30 | Update(ctx context.Context, firewall *Firewall, opts FirewallUpdateOpts) (*Firewall, *Response, error) 31 | // Delete deletes a Firewall. 32 | Delete(ctx context.Context, firewall *Firewall) (*Response, error) 33 | // SetRules sets the rules of a Firewall. 34 | SetRules(ctx context.Context, firewall *Firewall, opts FirewallSetRulesOpts) ([]*Action, *Response, error) 35 | ApplyResources(ctx context.Context, firewall *Firewall, resources []FirewallResource) ([]*Action, *Response, error) 36 | RemoveResources(ctx context.Context, firewall *Firewall, resources []FirewallResource) ([]*Action, *Response, error) 37 | } 38 | -------------------------------------------------------------------------------- /hcloud/zz_floating_ip_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IFloatingIPClient ... 10 | type IFloatingIPClient interface { 11 | // GetByID retrieves a Floating IP by its ID. If the Floating IP does not exist, 12 | // nil is returned. 13 | GetByID(ctx context.Context, id int64) (*FloatingIP, *Response, error) 14 | // GetByName retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned. 15 | GetByName(ctx context.Context, name string) (*FloatingIP, *Response, error) 16 | // Get retrieves a Floating IP by its ID if the input can be parsed as an integer, otherwise it 17 | // retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned. 18 | Get(ctx context.Context, idOrName string) (*FloatingIP, *Response, error) 19 | // List returns a list of Floating IPs for a specific page. 20 | // 21 | // Please note that filters specified in opts are not taken into account 22 | // when their value corresponds to their zero value or when they are empty. 23 | List(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, *Response, error) 24 | // All returns all Floating IPs. 25 | All(ctx context.Context) ([]*FloatingIP, error) 26 | // AllWithOpts returns all Floating IPs for the given options. 27 | AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) 28 | // Create creates a Floating IP. 29 | Create(ctx context.Context, opts FloatingIPCreateOpts) (FloatingIPCreateResult, *Response, error) 30 | // Delete deletes a Floating IP. 31 | Delete(ctx context.Context, floatingIP *FloatingIP) (*Response, error) 32 | // Update updates a Floating IP. 33 | Update(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPUpdateOpts) (*FloatingIP, *Response, error) 34 | // Assign assigns a Floating IP to a server. 35 | Assign(ctx context.Context, floatingIP *FloatingIP, server *Server) (*Action, *Response, error) 36 | // Unassign unassigns a Floating IP from the currently assigned server. 37 | Unassign(ctx context.Context, floatingIP *FloatingIP) (*Action, *Response, error) 38 | // ChangeDNSPtr changes or resets the reverse DNS pointer for a Floating IP address. 39 | // Pass a nil ptr to reset the reverse DNS pointer to its default value. 40 | ChangeDNSPtr(ctx context.Context, floatingIP *FloatingIP, ip string, ptr *string) (*Action, *Response, error) 41 | // ChangeProtection changes the resource protection level of a Floating IP. 42 | ChangeProtection(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPChangeProtectionOpts) (*Action, *Response, error) 43 | } 44 | -------------------------------------------------------------------------------- /hcloud/zz_image_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IImageClient ... 10 | type IImageClient interface { 11 | // GetByID retrieves an image by its ID. If the image does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Image, *Response, error) 13 | // GetByName retrieves an image by its name. If the image does not exist, nil is returned. 14 | // 15 | // Deprecated: Use [ImageClient.GetByNameAndArchitecture] instead. 16 | GetByName(ctx context.Context, name string) (*Image, *Response, error) 17 | // GetByNameAndArchitecture retrieves an image by its name and architecture. If the image does not exist, 18 | // nil is returned. 19 | // In contrast to [ImageClient.Get], this method also returns deprecated images. Depending on your needs you should 20 | // check for this in your calling method. 21 | GetByNameAndArchitecture(ctx context.Context, name string, architecture Architecture) (*Image, *Response, error) 22 | // Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it 23 | // retrieves an image by its name. If the image does not exist, nil is returned. 24 | // 25 | // Deprecated: Use [ImageClient.GetForArchitecture] instead. 26 | Get(ctx context.Context, idOrName string) (*Image, *Response, error) 27 | // GetForArchitecture retrieves an image by its ID if the input can be parsed as an integer, otherwise it 28 | // retrieves an image by its name and architecture. If the image does not exist, nil is returned. 29 | // 30 | // In contrast to [ImageClient.Get], this method also returns deprecated images. Depending on your needs you should 31 | // check for this in your calling method. 32 | GetForArchitecture(ctx context.Context, idOrName string, architecture Architecture) (*Image, *Response, error) 33 | // List returns a list of images for a specific page. 34 | // 35 | // Please note that filters specified in opts are not taken into account 36 | // when their value corresponds to their zero value or when they are empty. 37 | List(ctx context.Context, opts ImageListOpts) ([]*Image, *Response, error) 38 | // All returns all images. 39 | All(ctx context.Context) ([]*Image, error) 40 | // AllWithOpts returns all images for the given options. 41 | AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) 42 | // Delete deletes an image. 43 | Delete(ctx context.Context, image *Image) (*Response, error) 44 | // Update updates an image. 45 | Update(ctx context.Context, image *Image, opts ImageUpdateOpts) (*Image, *Response, error) 46 | // ChangeProtection changes the resource protection level of an image. 47 | ChangeProtection(ctx context.Context, image *Image, opts ImageChangeProtectionOpts) (*Action, *Response, error) 48 | } 49 | -------------------------------------------------------------------------------- /hcloud/zz_iso_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IISOClient ... 10 | type IISOClient interface { 11 | // GetByID retrieves an ISO by its ID. 12 | GetByID(ctx context.Context, id int64) (*ISO, *Response, error) 13 | // GetByName retrieves an ISO by its name. 14 | GetByName(ctx context.Context, name string) (*ISO, *Response, error) 15 | // Get retrieves an ISO by its ID if the input can be parsed as an integer, otherwise it retrieves an ISO by its name. 16 | Get(ctx context.Context, idOrName string) (*ISO, *Response, error) 17 | // List returns a list of ISOs for a specific page. 18 | // 19 | // Please note that filters specified in opts are not taken into account 20 | // when their value corresponds to their zero value or when they are empty. 21 | List(ctx context.Context, opts ISOListOpts) ([]*ISO, *Response, error) 22 | // All returns all ISOs. 23 | All(ctx context.Context) ([]*ISO, error) 24 | // AllWithOpts returns all ISOs for the given options. 25 | AllWithOpts(ctx context.Context, opts ISOListOpts) ([]*ISO, error) 26 | } 27 | -------------------------------------------------------------------------------- /hcloud/zz_load_balancer_type_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ILoadBalancerTypeClient ... 10 | type ILoadBalancerTypeClient interface { 11 | // GetByID retrieves a Load Balancer type by its ID. If the Load Balancer type does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*LoadBalancerType, *Response, error) 13 | // GetByName retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*LoadBalancerType, *Response, error) 15 | // Get retrieves a Load Balancer type by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*LoadBalancerType, *Response, error) 18 | // List returns a list of Load Balancer types for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, *Response, error) 23 | // All returns all Load Balancer types. 24 | All(ctx context.Context) ([]*LoadBalancerType, error) 25 | // AllWithOpts returns all Load Balancer types for the given options. 26 | AllWithOpts(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, error) 27 | } 28 | -------------------------------------------------------------------------------- /hcloud/zz_location_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ILocationClient ... 10 | type ILocationClient interface { 11 | // GetByID retrieves a location by its ID. If the location does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Location, *Response, error) 13 | // GetByName retrieves an location by its name. If the location does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*Location, *Response, error) 15 | // Get retrieves a location by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a location by its name. If the location does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*Location, *Response, error) 18 | // List returns a list of locations for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts LocationListOpts) ([]*Location, *Response, error) 23 | // All returns all locations. 24 | All(ctx context.Context) ([]*Location, error) 25 | // AllWithOpts returns all locations for the given options. 26 | AllWithOpts(ctx context.Context, opts LocationListOpts) ([]*Location, error) 27 | } 28 | -------------------------------------------------------------------------------- /hcloud/zz_network_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // INetworkClient ... 10 | type INetworkClient interface { 11 | // GetByID retrieves a network by its ID. If the network does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Network, *Response, error) 13 | // GetByName retrieves a network by its name. If the network does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*Network, *Response, error) 15 | // Get retrieves a network by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a network by its name. If the network does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*Network, *Response, error) 18 | // List returns a list of networks for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts NetworkListOpts) ([]*Network, *Response, error) 23 | // All returns all networks. 24 | All(ctx context.Context) ([]*Network, error) 25 | // AllWithOpts returns all networks for the given options. 26 | AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) 27 | // Delete deletes a network. 28 | Delete(ctx context.Context, network *Network) (*Response, error) 29 | // Update updates a network. 30 | Update(ctx context.Context, network *Network, opts NetworkUpdateOpts) (*Network, *Response, error) 31 | // Create creates a new network. 32 | Create(ctx context.Context, opts NetworkCreateOpts) (*Network, *Response, error) 33 | // ChangeIPRange changes the IP range of a network. 34 | ChangeIPRange(ctx context.Context, network *Network, opts NetworkChangeIPRangeOpts) (*Action, *Response, error) 35 | // AddSubnet adds a subnet to a network. 36 | AddSubnet(ctx context.Context, network *Network, opts NetworkAddSubnetOpts) (*Action, *Response, error) 37 | // DeleteSubnet deletes a subnet from a network. 38 | DeleteSubnet(ctx context.Context, network *Network, opts NetworkDeleteSubnetOpts) (*Action, *Response, error) 39 | // AddRoute adds a route to a network. 40 | AddRoute(ctx context.Context, network *Network, opts NetworkAddRouteOpts) (*Action, *Response, error) 41 | // DeleteRoute deletes a route from a network. 42 | DeleteRoute(ctx context.Context, network *Network, opts NetworkDeleteRouteOpts) (*Action, *Response, error) 43 | // ChangeProtection changes the resource protection level of a network. 44 | ChangeProtection(ctx context.Context, network *Network, opts NetworkChangeProtectionOpts) (*Action, *Response, error) 45 | } 46 | -------------------------------------------------------------------------------- /hcloud/zz_placement_group_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IPlacementGroupClient ... 10 | type IPlacementGroupClient interface { 11 | // GetByID retrieves a PlacementGroup by its ID. If the PlacementGroup does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*PlacementGroup, *Response, error) 13 | // GetByName retrieves a PlacementGroup by its name. If the PlacementGroup does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*PlacementGroup, *Response, error) 15 | // Get retrieves a PlacementGroup by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a PlacementGroup by its name. If the PlacementGroup does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*PlacementGroup, *Response, error) 18 | // List returns a list of PlacementGroups for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, *Response, error) 23 | // All returns all PlacementGroups. 24 | All(ctx context.Context) ([]*PlacementGroup, error) 25 | // AllWithOpts returns all PlacementGroups for the given options. 26 | AllWithOpts(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, error) 27 | // Create creates a new PlacementGroup. 28 | Create(ctx context.Context, opts PlacementGroupCreateOpts) (PlacementGroupCreateResult, *Response, error) 29 | // Update updates a PlacementGroup. 30 | Update(ctx context.Context, placementGroup *PlacementGroup, opts PlacementGroupUpdateOpts) (*PlacementGroup, *Response, error) 31 | // Delete deletes a PlacementGroup. 32 | Delete(ctx context.Context, placementGroup *PlacementGroup) (*Response, error) 33 | } 34 | -------------------------------------------------------------------------------- /hcloud/zz_pricing_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IPricingClient ... 10 | type IPricingClient interface { 11 | // Get retrieves pricing information. 12 | Get(ctx context.Context) (Pricing, *Response, error) 13 | } 14 | -------------------------------------------------------------------------------- /hcloud/zz_primary_ip_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IPrimaryIPClient ... 10 | type IPrimaryIPClient interface { 11 | // GetByID retrieves a Primary IP by its ID. If the Primary IP does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*PrimaryIP, *Response, error) 13 | // GetByIP retrieves a Primary IP by its IP Address. If the Primary IP does not exist, nil is returned. 14 | GetByIP(ctx context.Context, ip string) (*PrimaryIP, *Response, error) 15 | // GetByName retrieves a Primary IP by its name. If the Primary IP does not exist, nil is returned. 16 | GetByName(ctx context.Context, name string) (*PrimaryIP, *Response, error) 17 | // Get retrieves a Primary IP by its ID if the input can be parsed as an integer, otherwise it 18 | // retrieves a Primary IP by its name. If the Primary IP does not exist, nil is returned. 19 | Get(ctx context.Context, idOrName string) (*PrimaryIP, *Response, error) 20 | // List returns a list of Primary IPs for a specific page. 21 | // 22 | // Please note that filters specified in opts are not taken into account 23 | // when their value corresponds to their zero value or when they are empty. 24 | List(ctx context.Context, opts PrimaryIPListOpts) ([]*PrimaryIP, *Response, error) 25 | // All returns all Primary IPs. 26 | All(ctx context.Context) ([]*PrimaryIP, error) 27 | // AllWithOpts returns all Primary IPs for the given options. 28 | AllWithOpts(ctx context.Context, opts PrimaryIPListOpts) ([]*PrimaryIP, error) 29 | // Create creates a Primary IP. 30 | Create(ctx context.Context, opts PrimaryIPCreateOpts) (*PrimaryIPCreateResult, *Response, error) 31 | // Delete deletes a Primary IP. 32 | Delete(ctx context.Context, primaryIP *PrimaryIP) (*Response, error) 33 | // Update updates a Primary IP. 34 | Update(ctx context.Context, primaryIP *PrimaryIP, opts PrimaryIPUpdateOpts) (*PrimaryIP, *Response, error) 35 | // Assign a Primary IP to a resource. 36 | Assign(ctx context.Context, opts PrimaryIPAssignOpts) (*Action, *Response, error) 37 | // Unassign a Primary IP from a resource. 38 | Unassign(ctx context.Context, id int64) (*Action, *Response, error) 39 | // ChangeDNSPtr Change the reverse DNS from a Primary IP. 40 | ChangeDNSPtr(ctx context.Context, opts PrimaryIPChangeDNSPtrOpts) (*Action, *Response, error) 41 | // ChangeProtection Changes the protection configuration of a Primary IP. 42 | ChangeProtection(ctx context.Context, opts PrimaryIPChangeProtectionOpts) (*Action, *Response, error) 43 | } 44 | -------------------------------------------------------------------------------- /hcloud/zz_rdns_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | "net" 8 | ) 9 | 10 | // IRDNSClient ... 11 | type IRDNSClient interface { 12 | // ChangeDNSPtr changes or resets the reverse DNS pointer for a IP address. 13 | // Pass a nil ptr to reset the reverse DNS pointer to its default value. 14 | ChangeDNSPtr(ctx context.Context, rdns RDNSSupporter, ip net.IP, ptr *string) (*Action, *Response, error) 15 | } 16 | -------------------------------------------------------------------------------- /hcloud/zz_resource_action_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IResourceActionClient ... 10 | type IResourceActionClient interface { 11 | // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Action, *Response, error) 13 | // List returns a list of actions for a specific page. 14 | // 15 | // Please note that filters specified in opts are not taken into account 16 | // when their value corresponds to their zero value or when they are empty. 17 | List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) 18 | // All returns all actions for the given options. 19 | All(ctx context.Context, opts ActionListOpts) ([]*Action, error) 20 | } 21 | -------------------------------------------------------------------------------- /hcloud/zz_server_type_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IServerTypeClient ... 10 | type IServerTypeClient interface { 11 | // GetByID retrieves a server type by its ID. If the server type does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*ServerType, *Response, error) 13 | // GetByName retrieves a server type by its name. If the server type does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*ServerType, *Response, error) 15 | // Get retrieves a server type by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a server type by its name. If the server type does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*ServerType, *Response, error) 18 | // List returns a list of server types for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, *Response, error) 23 | // All returns all server types. 24 | All(ctx context.Context) ([]*ServerType, error) 25 | // AllWithOpts returns all server types for the given options. 26 | AllWithOpts(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, error) 27 | } 28 | -------------------------------------------------------------------------------- /hcloud/zz_ssh_key_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // ISSHKeyClient ... 10 | type ISSHKeyClient interface { 11 | // GetByID retrieves a SSH key by its ID. If the SSH key does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*SSHKey, *Response, error) 13 | // GetByName retrieves a SSH key by its name. If the SSH key does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*SSHKey, *Response, error) 15 | // GetByFingerprint retreives a SSH key by its fingerprint. If the SSH key does not exist, nil is returned. 16 | GetByFingerprint(ctx context.Context, fingerprint string) (*SSHKey, *Response, error) 17 | // Get retrieves a SSH key by its ID if the input can be parsed as an integer, otherwise it 18 | // retrieves a SSH key by its name. If the SSH key does not exist, nil is returned. 19 | Get(ctx context.Context, idOrName string) (*SSHKey, *Response, error) 20 | // List returns a list of SSH keys for a specific page. 21 | // 22 | // Please note that filters specified in opts are not taken into account 23 | // when their value corresponds to their zero value or when they are empty. 24 | List(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, *Response, error) 25 | // All returns all SSH keys. 26 | All(ctx context.Context) ([]*SSHKey, error) 27 | // AllWithOpts returns all SSH keys with the given options. 28 | AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) 29 | // Create creates a new SSH key with the given options. 30 | Create(ctx context.Context, opts SSHKeyCreateOpts) (*SSHKey, *Response, error) 31 | // Delete deletes a SSH key. 32 | Delete(ctx context.Context, sshKey *SSHKey) (*Response, error) 33 | // Update updates a SSH key. 34 | Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUpdateOpts) (*SSHKey, *Response, error) 35 | } 36 | -------------------------------------------------------------------------------- /hcloud/zz_volume_client_iface.go: -------------------------------------------------------------------------------- 1 | // Code generated by ifacemaker; DO NOT EDIT. 2 | 3 | package hcloud 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // IVolumeClient ... 10 | type IVolumeClient interface { 11 | // GetByID retrieves a volume by its ID. If the volume does not exist, nil is returned. 12 | GetByID(ctx context.Context, id int64) (*Volume, *Response, error) 13 | // GetByName retrieves a volume by its name. If the volume does not exist, nil is returned. 14 | GetByName(ctx context.Context, name string) (*Volume, *Response, error) 15 | // Get retrieves a volume by its ID if the input can be parsed as an integer, otherwise it 16 | // retrieves a volume by its name. If the volume does not exist, nil is returned. 17 | Get(ctx context.Context, idOrName string) (*Volume, *Response, error) 18 | // List returns a list of volumes for a specific page. 19 | // 20 | // Please note that filters specified in opts are not taken into account 21 | // when their value corresponds to their zero value or when they are empty. 22 | List(ctx context.Context, opts VolumeListOpts) ([]*Volume, *Response, error) 23 | // All returns all volumes. 24 | All(ctx context.Context) ([]*Volume, error) 25 | // AllWithOpts returns all volumes with the given options. 26 | AllWithOpts(ctx context.Context, opts VolumeListOpts) ([]*Volume, error) 27 | // Create creates a new volume with the given options. 28 | Create(ctx context.Context, opts VolumeCreateOpts) (VolumeCreateResult, *Response, error) 29 | // Delete deletes a volume. 30 | Delete(ctx context.Context, volume *Volume) (*Response, error) 31 | // Update updates a volume. 32 | Update(ctx context.Context, volume *Volume, opts VolumeUpdateOpts) (*Volume, *Response, error) 33 | // AttachWithOpts attaches a volume to a server. 34 | AttachWithOpts(ctx context.Context, volume *Volume, opts VolumeAttachOpts) (*Action, *Response, error) 35 | // Attach attaches a volume to a server. 36 | Attach(ctx context.Context, volume *Volume, server *Server) (*Action, *Response, error) 37 | // Detach detaches a volume from a server. 38 | Detach(ctx context.Context, volume *Volume) (*Action, *Response, error) 39 | // ChangeProtection changes the resource protection level of a volume. 40 | ChangeProtection(ctx context.Context, volume *Volume, opts VolumeChangeProtectionOpts) (*Action, *Response, error) 41 | // Resize changes the size of a volume. 42 | Resize(ctx context.Context, volume *Volume, size int) (*Action, *Response, error) 43 | } 44 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>hetznercloud/.github//renovate/default", 5 | "github>hetznercloud/.github//renovate/golang" 6 | ], 7 | "postUpdateOptions": ["gomodTidy"] 8 | } 9 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/jmattheis/goverter/cmd/goverter" 8 | _ "github.com/vburenin/ifacemaker" 9 | ) 10 | --------------------------------------------------------------------------------