pax_global_header00006660000000000000000000000064135622605430014520gustar00rootroot0000000000000052 comment=f7f7f7d81d61ce1e72939c7c8a5d2aff6b7c0da3 hcloud-go-1.17.0/000077500000000000000000000000001356226054300134675ustar00rootroot00000000000000hcloud-go-1.17.0/.github/000077500000000000000000000000001356226054300150275ustar00rootroot00000000000000hcloud-go-1.17.0/.github/workflows/000077500000000000000000000000001356226054300170645ustar00rootroot00000000000000hcloud-go-1.17.0/.github/workflows/ci.yml000066400000000000000000000016011356226054300202000ustar00rootroot00000000000000name: Continuous Integration on: [push, pull_request] jobs: build: name: Build runs-on: ubuntu-latest strategy: matrix: go-version: [1.12, 1.13] steps: - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} - name: Check out code into the Go module directory uses: actions/checkout@v1 - name: Get dependencies run: | go get -u golang.org/x/lint/golint go get -v -t -d ./... - name: Run go fmt run: diff -u <(echo -n) <(gofmt -d -s .) - name: Run go vet run: go vet ./... - name: Run go lint run: | # Temporary fix for: # https://github.com/actions/setup-go/issues/14 export PATH=$PATH:$(go env GOPATH)/bin golint -set_exit_status ./... - name: Run tests run: go test -v ./... hcloud-go-1.17.0/CHANGES.md000066400000000000000000000053631356226054300150700ustar00rootroot00000000000000# Changes ## v1.17.0 * Add `Created` field to `SSHKey` ## v1.16.0 * Make IP range optional when adding a subnet to a network * Add support for names to Floating IPs ## v1.15.1 * Rename `MacAddress` to `MACAddress` on `ServerPrivateNet` ## v1.15.0 * Add `MacAddress` field to `ServerPrivateNet` * Add `WithDebugWriter()` client option to provide an `io.Writer` to write debug output to ## v1.14.0 * Add `Created` field to `FloatingIP` * Add support for networks ## v1.13.0 * Add missing fields to `*ListOpts` structs * Fix error handling in `WatchProgress()` * Add support for filtering volumes, images, and servers by status ## v1.12.0 * Add missing constants for all [documented error codes](https://docs.hetzner.cloud/#overview-errors) * Add support for automounting volumes * Add support for attaching volumes when creating a server ## v1.11.0 * Add `NextActions` to `ServerCreateResult` and `VolumeCreateResult` ## v1.10.0 * Add `WithApplication()` client option to provide an application name and version that will be included in the `User-Agent` HTTP header * Add support for volumes ## v1.9.0 * Add `AllWithOpts()` to server, Floating IP, image, and SSH key client * Expose labels of servers, Floating IPs, images, and SSH Keys ## v1.8.0 * Add `WithPollInterval()` option to `Client` which allows to specify the polling interval ([issue #92](https://github.com/hetznercloud/hcloud-go/issues/92)) * Add `CPUType` field to `ServerType` ([issue #91](https://github.com/hetznercloud/hcloud-go/pull/91)) ## v1.7.0 * Add `Deprecated ` field to `Image` ([issue #88](https://github.com/hetznercloud/hcloud-go/issues/88)) * Add `StartAfterCreate` flag to `ServerCreateOpts` ([issue #87](https://github.com/hetznercloud/hcloud-go/issues/87)) * Fix enum types ([issue #89](https://github.com/hetznercloud/hcloud-go/issues/89)) ## v1.6.0 * Add `ChangeProtection()` to server, Floating IP, and image client * Expose protection of servers, Floating IPs, and images ## v1.5.0 * Add `GetByFingerprint()` to SSH key client ## v1.4.0 * Retry all calls that triggered the API ratelimit * Slow down `WatchProgress()` in action client from 100ms polling interval to 500ms ## v1.3.1 * Make clients using the old error code for ratelimiting work as expected ([issue #73](https://github.com/hetznercloud/hcloud-go/issues/73)) ## v1.3.0 * Support passing user data on server creation ([issue #70](https://github.com/hetznercloud/hcloud-go/issues/70)) * Fix leaking response body by not closing it ([issue #68](https://github.com/hetznercloud/hcloud-go/issues/68)) ## v1.2.0 * Add `WatchProgress()` to action client * Use correct error code for ratelimit error (deprecated `ErrorCodeLimitReached`, added `ErrorCodeRateLimitExceeded`) ## v1.1.0 * Add `Image` field to `Server` hcloud-go-1.17.0/LICENSE000066400000000000000000000020631356226054300144750ustar00rootroot00000000000000MIT License Copyright (c) 2018 Hetzner Cloud GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hcloud-go-1.17.0/README.md000066400000000000000000000022001356226054300147400ustar00rootroot00000000000000# hcloud: A Go library for the Hetzner Cloud API [![GitHub Actions status](https://github.com/hetznercloud/hcloud-go/workflows/Continuous%20Integration/badge.svg)](https://github.com/hetznercloud/hcloud-go/actions) [![GoDoc](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud?status.svg)](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud) Package hcloud is a library for the Hetzner Cloud API. The library’s documentation is available at [GoDoc](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud), the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud/). ## Example ```go package main import ( "context" "fmt" "log" "github.com/hetznercloud/hcloud-go/hcloud" ) func main() { client := hcloud.NewClient(hcloud.WithToken("token")) server, _, err := client.Server.GetByID(context.Background(), 1) if err != nil { log.Fatalf("error retrieving server: %s\n", err) } if server != nil { fmt.Printf("server 1 is called %q\n", server.Name) } else { fmt.Println("server 1 not found") } } ``` ## License MIT license hcloud-go-1.17.0/go.mod000066400000000000000000000000621356226054300145730ustar00rootroot00000000000000module github.com/hetznercloud/hcloud-go go 1.12 hcloud-go-1.17.0/hcloud/000077500000000000000000000000001356226054300147455ustar00rootroot00000000000000hcloud-go-1.17.0/hcloud/action.go000066400000000000000000000110071356226054300165500ustar00rootroot00000000000000package hcloud import ( "context" "fmt" "net/url" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Action represents an action in the Hetzner Cloud. type Action struct { ID int Status ActionStatus Command string Progress int Started time.Time Finished time.Time ErrorCode string ErrorMessage string Resources []*ActionResource } // ActionStatus represents an action's status. type ActionStatus string // List of action statuses. const ( ActionStatusRunning ActionStatus = "running" ActionStatusSuccess ActionStatus = "success" ActionStatusError ActionStatus = "error" ) // ActionResource references other resources from an action. type ActionResource struct { ID int Type ActionResourceType } // ActionResourceType represents an action's resource reference type. type ActionResourceType string // List of action resource reference types. const ( ActionResourceTypeServer ActionResourceType = "server" ActionResourceTypeImage ActionResourceType = "image" ActionResourceTypeISO ActionResourceType = "iso" ActionResourceTypeFloatingIP ActionResourceType = "floating_ip" ActionResourceTypeVolume ActionResourceType = "volume" ) // ActionError is the error of an action. type ActionError struct { Code string Message string } func (e ActionError) Error() string { return fmt.Sprintf("%s (%s)", e.Message, e.Code) } func (a *Action) Error() error { if a.ErrorCode != "" && a.ErrorMessage != "" { return ActionError{ Code: a.ErrorCode, Message: a.ErrorMessage, } } return nil } // ActionClient is a client for the actions API. type ActionClient struct { client *Client } // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. func (c *ActionClient) GetByID(ctx context.Context, id int) (*Action, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/actions/%d", id), nil) if err != nil { return nil, nil, err } var body schema.ActionGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return ActionFromSchema(body.Action), resp, nil } // ActionListOpts specifies options for listing actions. type ActionListOpts struct { ListOpts Status []ActionStatus Sort []string } func (l ActionListOpts) values() url.Values { vals := l.ListOpts.values() for _, status := range l.Status { vals.Add("status", string(status)) } for _, sort := range l.Sort { vals.Add("sort", sort) } return vals } // List returns a list of actions for a specific page. func (c *ActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) { path := "/actions?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.ActionListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } actions := make([]*Action, 0, len(body.Actions)) for _, i := range body.Actions { actions = append(actions, ActionFromSchema(i)) } return actions, resp, nil } // All returns all actions. func (c *ActionClient) All(ctx context.Context) ([]*Action, error) { allActions := []*Action{} opts := ActionListOpts{} opts.PerPage = 50 _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page actions, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allActions = append(allActions, actions...) return resp, nil }) if err != nil { return nil, err } return allActions, nil } // WatchProgress watches the action's progress until it completes with success or error. func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) { errCh := make(chan error, 1) progressCh := make(chan int) go func() { defer close(errCh) defer close(progressCh) ticker := time.NewTicker(c.client.pollInterval) sendProgress := func(p int) { select { case progressCh <- p: break default: break } } for { select { case <-ctx.Done(): errCh <- ctx.Err() return case <-ticker.C: break } a, _, err := c.GetByID(ctx, action.ID) if err != nil { errCh <- err return } switch a.Status { case ActionStatusRunning: sendProgress(a.Progress) break case ActionStatusSuccess: sendProgress(100) errCh <- nil return case ActionStatusError: errCh <- a.Error() return } } }() return progressCh, errCh } hcloud-go-1.17.0/hcloud/action_test.go000066400000000000000000000142631356226054300176160ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestActionClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ActionGetResponse{ Action: schema.Action{ ID: 1, Status: "running", Command: "create_server", Progress: 50, Started: time.Date(2017, 12, 4, 14, 31, 1, 0, time.UTC), }, }) }) ctx := context.Background() action, _, err := env.Client.Action.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if action == nil { t.Fatal("no action") } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestActionClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() action, _, err := env.Client.Action.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if action != nil { t.Fatal("expected no action") } } func TestActionClientList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } status := r.URL.Query()["status"] if len(status) != 2 { t.Errorf("expected status to contain 2 elements; got %q", status) } else { if status[0] != "running" { t.Errorf("expected status[0] to be running; got %q", status[0]) } if status[1] != "error" { t.Errorf("expected status[1] to be error; got %q", status[1]) } } sort := r.URL.Query()["sort"] if len(sort) != 3 { t.Errorf("expected sort to contain 3 elements; got %q", sort) } else { if sort[0] != "status" { t.Errorf("expected sort[0] to be status; got %q", sort[0]) } if sort[1] != "progress:desc" { t.Errorf("expected sort[1] to be progress:desc; got %q", sort[1]) } if sort[2] != "command:asc" { t.Errorf("expected sort[2] to be command:asc; got %q", sort[2]) } } json.NewEncoder(w).Encode(schema.ActionListResponse{ Actions: []schema.Action{ {ID: 1}, {ID: 2}, }, }) }) opts := ActionListOpts{} opts.Page = 2 opts.PerPage = 50 opts.Status = []ActionStatus{ActionStatusRunning, ActionStatusError} opts.Sort = []string{"status", "progress:desc", "command:asc"} ctx := context.Background() actions, _, err := env.Client.Action.List(ctx, opts) if err != nil { t.Fatal(err) } if len(actions) != 2 { t.Fatal("expected 2 actions") } } func TestActionClientAll(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Actions []schema.Action `json:"actions"` Meta schema.Meta `json:"meta"` }{ Actions: []schema.Action{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() actions, err := env.Client.Action.All(ctx) if err != nil { t.Fatal(err) } if len(actions) != 3 { t.Fatalf("expected 3 actions; got %d", len(actions)) } if actions[0].ID != 1 || actions[1].ID != 2 || actions[2].ID != 3 { t.Errorf("unexpected actions") } } func TestActionClientWatchProgress(t *testing.T) { env := newTestEnv() defer env.Teardown() callCount := 0 env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { callCount++ w.Header().Set("Content-Type", "application/json") switch callCount { case 1: json.NewEncoder(w).Encode(schema.ActionGetResponse{ Action: schema.Action{ ID: 1, Status: "running", Progress: 50, }, }) case 2: w.WriteHeader(http.StatusTooManyRequests) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeRateLimitExceeded), Message: "ratelimited", }, }) return case 3: json.NewEncoder(w).Encode(schema.ActionGetResponse{ Action: schema.Action{ ID: 1, Status: "error", Progress: 100, Error: &schema.ActionError{ Code: "action_failed", Message: "action failed", }, }, }) default: t.Errorf("unexpected number of calls to the test server: %v", callCount) } }) action := &Action{ ID: 1, Status: ActionStatusRunning, Progress: 0, } ctx := context.Background() progressCh, errCh := env.Client.Action.WatchProgress(ctx, action) var ( progressUpdates []int err error ) loop: for { select { case progress := <-progressCh: progressUpdates = append(progressUpdates, progress) case err = <-errCh: break loop } } if err == nil { t.Fatal("expected an error") } if e, ok := err.(ActionError); !ok || e.Code != "action_failed" { t.Fatalf("expected hcloud.Error, but got: %#v", err) } if len(progressUpdates) != 1 || progressUpdates[0] != 50 { t.Fatalf("unexpected progress updates: %v", progressUpdates) } } func TestActionClientWatchProgressError(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeServiceError), Message: "service error", }, }) }) action := &Action{ID: 1} ctx := context.Background() _, errCh := env.Client.Action.WatchProgress(ctx, action) if err := <-errCh; err == nil { t.Fatal("expected an error") } } hcloud-go-1.17.0/hcloud/client.go000066400000000000000000000223161356226054300165560ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "math" "net/http" "net/http/httputil" "net/url" "strconv" "strings" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Endpoint is the base URL of the API. const Endpoint = "https://api.hetzner.cloud/v1" // UserAgent is the value for the library part of the User-Agent header // that is sent with each request. const UserAgent = "hcloud-go/" + Version // A BackoffFunc returns the duration to wait before performing the // next retry. The retries argument specifies how many retries have // already been performed. When called for the first time, retries is 0. type BackoffFunc func(retries int) time.Duration // ConstantBackoff returns a BackoffFunc which backs off for // constant duration d. func ConstantBackoff(d time.Duration) BackoffFunc { return func(_ int) time.Duration { return d } } // ExponentialBackoff returns a BackoffFunc which implements an exponential // backoff using the formula: b^retries * d func ExponentialBackoff(b float64, d time.Duration) BackoffFunc { return func(retries int) time.Duration { return time.Duration(math.Pow(b, float64(retries))) * d } } // Client is a client for the Hetzner Cloud API. type Client struct { endpoint string token string pollInterval time.Duration backoffFunc BackoffFunc httpClient *http.Client applicationName string applicationVersion string userAgent string debugWriter io.Writer Action ActionClient Datacenter DatacenterClient FloatingIP FloatingIPClient Image ImageClient ISO ISOClient Location LocationClient Network NetworkClient Pricing PricingClient Server ServerClient ServerType ServerTypeClient SSHKey SSHKeyClient Volume VolumeClient } // A ClientOption is used to configure a Client. type ClientOption func(*Client) // WithEndpoint configures a Client to use the specified API endpoint. func WithEndpoint(endpoint string) ClientOption { return func(client *Client) { client.endpoint = strings.TrimRight(endpoint, "/") } } // WithToken configures a Client to use the specified token for authentication. func WithToken(token string) ClientOption { return func(client *Client) { client.token = token } } // WithPollInterval configures a Client to use the specified interval when polling // from the API. func WithPollInterval(pollInterval time.Duration) ClientOption { return func(client *Client) { client.pollInterval = pollInterval } } // WithBackoffFunc configures a Client to use the specified backoff function. func WithBackoffFunc(f BackoffFunc) ClientOption { return func(client *Client) { client.backoffFunc = f } } // WithApplication configures a Client with the given application name and // application version. The version may be blank. Programs are encouraged // to at least set an application name. func WithApplication(name, version string) ClientOption { return func(client *Client) { client.applicationName = name client.applicationVersion = version } } // WithDebugWriter configures a Client to print debug information to the given // writer. To, for example, print debug information on stderr, set it to os.Stderr. func WithDebugWriter(debugWriter io.Writer) ClientOption { return func(client *Client) { client.debugWriter = debugWriter } } // NewClient creates a new client. func NewClient(options ...ClientOption) *Client { client := &Client{ endpoint: Endpoint, httpClient: &http.Client{}, backoffFunc: ExponentialBackoff(2, 500*time.Millisecond), pollInterval: 500 * time.Millisecond, } for _, option := range options { option(client) } client.buildUserAgent() client.Action = ActionClient{client: client} client.Datacenter = DatacenterClient{client: client} client.FloatingIP = FloatingIPClient{client: client} client.Image = ImageClient{client: client} client.ISO = ISOClient{client: client} client.Location = LocationClient{client: client} client.Network = NetworkClient{client: client} client.Pricing = PricingClient{client: client} client.Server = ServerClient{client: client} client.ServerType = ServerTypeClient{client: client} client.SSHKey = SSHKeyClient{client: client} client.Volume = VolumeClient{client: client} return client } // NewRequest creates an HTTP request against the API. The returned request // is assigned with ctx and has all necessary headers set (auth, user agent, etc.). func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { url := c.endpoint + path req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.Header.Set("User-Agent", c.userAgent) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) if body != nil { req.Header.Set("Content-Type", "application/json") } req = req.WithContext(ctx) return req, nil } // Do performs an HTTP request against the API. func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { var retries int for { resp, err := c.httpClient.Do(r) if err != nil { return nil, err } response := &Response{Response: resp} body, err := ioutil.ReadAll(resp.Body) if err != nil { resp.Body.Close() return response, err } resp.Body.Close() resp.Body = ioutil.NopCloser(bytes.NewReader(body)) if c.debugWriter != nil { dumpReq, err := httputil.DumpRequest(r, true) if err != nil { return nil, err } fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq) dumpResp, err := httputil.DumpResponse(resp, true) if err != nil { return nil, err } fmt.Fprintf(c.debugWriter, "--- Response:\n%s\n\n", dumpResp) } if err = response.readMeta(body); err != nil { return response, fmt.Errorf("hcloud: error reading response meta data: %s", err) } if resp.StatusCode >= 400 && resp.StatusCode <= 599 { err = errorFromResponse(resp, body) if err == nil { err = fmt.Errorf("hcloud: server responded with status code %d", resp.StatusCode) } else { if err, ok := err.(Error); ok && err.Code == ErrorCodeRateLimitExceeded { c.backoff(retries) retries++ continue } } return response, err } if v != nil { if w, ok := v.(io.Writer); ok { _, err = io.Copy(w, bytes.NewReader(body)) } else { err = json.Unmarshal(body, v) } } return response, err } } func (c *Client) backoff(retries int) { time.Sleep(c.backoffFunc(retries)) } func (c *Client) all(f func(int) (*Response, error)) (*Response, error) { var ( page = 1 ) for { resp, err := f(page) if err != nil { return nil, err } if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 { return resp, nil } page = resp.Meta.Pagination.NextPage } } func (c *Client) buildUserAgent() { switch { case c.applicationName != "" && c.applicationVersion != "": c.userAgent = c.applicationName + "/" + c.applicationVersion + " " + UserAgent case c.applicationName != "" && c.applicationVersion == "": c.userAgent = c.applicationName + " " + UserAgent default: c.userAgent = UserAgent } } func errorFromResponse(resp *http.Response, body []byte) error { if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { return nil } var respBody schema.ErrorResponse if err := json.Unmarshal(body, &respBody); err != nil { return nil } if respBody.Error.Code == "" && respBody.Error.Message == "" { return nil } return ErrorFromSchema(respBody.Error) } // Response represents a response from the API. It embeds http.Response. type Response struct { *http.Response Meta Meta } func (r *Response) readMeta(body []byte) error { if h := r.Header.Get("RateLimit-Limit"); h != "" { r.Meta.Ratelimit.Limit, _ = strconv.Atoi(h) } if h := r.Header.Get("RateLimit-Remaining"); h != "" { r.Meta.Ratelimit.Remaining, _ = strconv.Atoi(h) } if h := r.Header.Get("RateLimit-Reset"); h != "" { if ts, err := strconv.ParseInt(h, 10, 64); err == nil { r.Meta.Ratelimit.Reset = time.Unix(ts, 0) } } if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { var s schema.MetaResponse if err := json.Unmarshal(body, &s); err != nil { return err } if s.Meta.Pagination != nil { p := PaginationFromSchema(*s.Meta.Pagination) r.Meta.Pagination = &p } } return nil } // Meta represents meta information included in an API response. type Meta struct { Pagination *Pagination Ratelimit Ratelimit } // Pagination represents pagination meta information. type Pagination struct { Page int PerPage int PreviousPage int NextPage int LastPage int TotalEntries int } // Ratelimit represents ratelimit information. type Ratelimit struct { Limit int Remaining int Reset time.Time } // ListOpts specifies options for listing resources. type ListOpts struct { Page int // Page (starting at 1) PerPage int // Items per page (0 means default) LabelSelector string // Label selector for filtering by labels } func (l ListOpts) values() url.Values { vals := url.Values{} if l.Page > 0 { vals.Add("page", strconv.Itoa(l.Page)) } if l.PerPage > 0 { vals.Add("per_page", strconv.Itoa(l.PerPage)) } if len(l.LabelSelector) > 0 { vals.Add("label_selector", l.LabelSelector) } return vals } hcloud-go-1.17.0/hcloud/client_test.go000066400000000000000000000154741356226054300176240ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) type testEnv struct { Server *httptest.Server Mux *http.ServeMux Client *Client } func (env *testEnv) Teardown() { env.Server.Close() env.Server = nil env.Mux = nil env.Client = nil } func newTestEnv() testEnv { mux := http.NewServeMux() server := httptest.NewServer(mux) client := NewClient( WithEndpoint(server.URL), WithToken("token"), WithBackoffFunc(func(_ int) time.Duration { return 0 }), ) return testEnv{ Server: server, Mux: mux, Client: client, } } func TestClientEndpointTrailingSlashesRemoved(t *testing.T) { client := NewClient(WithEndpoint("http://api/v1.0/////")) if strings.HasSuffix(client.endpoint, "/") { t.Fatalf("endpoint has trailing slashes: %q", client.endpoint) } } func TestClientError(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: "service_error", Message: "An error occured", }, }) }) ctx := context.Background() req, err := env.Client.NewRequest(ctx, "GET", "/error", nil) if err != nil { t.Fatalf("error creating request: %s", err) } _, err = env.Client.Do(req, nil) if _, ok := err.(Error); !ok { t.Fatalf("unexpected error of type %T: %v", err, err) } apiError := err.(Error) if apiError.Code != "service_error" { t.Errorf("unexpected error code: %q", apiError.Code) } if apiError.Message != "An error occured" { t.Errorf("unexpected error message: %q", apiError.Message) } } func TestClientMeta(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("RateLimit-Limit", "1000") w.Header().Set("RateLimit-Remaining", "999") w.Header().Set("RateLimit-Reset", "1511954577") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{ "foo": "bar", "meta": { "pagination": { "page": 1 } } }`) }) ctx := context.Background() req, err := env.Client.NewRequest(ctx, "GET", "/", nil) if err != nil { t.Fatalf("error creating request: %s", err) } response, err := env.Client.Do(req, nil) if err != nil { t.Fatalf("request failed: %s", err) } if response.Meta.Ratelimit.Limit != 1000 { t.Errorf("unexpected ratelimit limit: %d", response.Meta.Ratelimit.Limit) } if response.Meta.Ratelimit.Remaining != 999 { t.Errorf("unexpected ratelimit remaining: %d", response.Meta.Ratelimit.Remaining) } if !response.Meta.Ratelimit.Reset.Equal(time.Unix(1511954577, 0)) { t.Errorf("unexpected ratelimit reset: %v", response.Meta.Ratelimit.Reset) } if response.Meta.Pagination.Page != 1 { t.Error("missing pagination") } } func TestClientMetaNonJSON(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) fmt.Fprint(w, "foo") }) ctx := context.Background() req, err := env.Client.NewRequest(ctx, "GET", "/", nil) if err != nil { t.Fatalf("error creating request: %s", err) } response, err := env.Client.Do(req, nil) if err != nil { t.Fatalf("request failed: %s", err) } if response.Meta.Pagination != nil { t.Fatal("pagination should not be present") } } func TestClientAll(t *testing.T) { env := newTestEnv() defer env.Teardown() var ( ctx = context.Background() ratelimited bool expectedPage = 1 ) env.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") respBody := schema.MetaResponse{ Meta: schema.Meta{ Pagination: &schema.MetaPagination{ LastPage: 3, PerPage: 1, TotalEntries: 3, }, }, } switch page := r.URL.Query().Get("page"); page { case "", "1": respBody.Meta.Pagination.Page = 1 respBody.Meta.Pagination.NextPage = 2 case "2": if !ratelimited { ratelimited = true w.WriteHeader(http.StatusTooManyRequests) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeRateLimitExceeded), Message: "ratelimited", }, }) return } respBody.Meta.Pagination.Page = 2 respBody.Meta.Pagination.PreviousPage = 1 respBody.Meta.Pagination.NextPage = 3 case "3": respBody.Meta.Pagination.Page = 3 respBody.Meta.Pagination.PreviousPage = 2 default: t.Errorf("bad page: %q", page) } json.NewEncoder(w).Encode(respBody) }) env.Client.all(func(page int) (*Response, error) { if page != expectedPage { t.Fatalf("expected page %d, but called for %d", expectedPage, page) } path := fmt.Sprintf("/?page=%d&per_page=1", page) req, err := env.Client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, err } resp, err := env.Client.Do(req, nil) if err != nil { return resp, err } expectedPage++ return resp, err }) if expectedPage != 4 { t.Errorf("expected to have walked through 3 pages, but walked through %d pages", expectedPage-1) } } func TestClientDo(t *testing.T) { env := newTestEnv() defer env.Teardown() callCount := 0 env.Mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { callCount++ w.Header().Set("Content-Type", "application/json") switch callCount { case 1: w.WriteHeader(http.StatusTooManyRequests) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeRateLimitExceeded), Message: "ratelimited", }, }) case 2: fmt.Fprintln(w, "{}") default: t.Errorf("unexpected number of calls to the test server: %v", callCount) } }) ctx := context.Background() request, _ := env.Client.NewRequest(ctx, http.MethodGet, "/test", nil) _, err := env.Client.Do(request, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestBuildUserAgent(t *testing.T) { testCases := []struct { name string applicationName string applicationVersion string userAgent string }{ {"with application name and version", "test", "1.0", "test/1.0 " + UserAgent}, {"with application name but no version", "test", "", "test " + UserAgent}, {"without application name and version", "", "", UserAgent}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { client := NewClient(WithApplication(testCase.applicationName, testCase.applicationVersion)) if client.userAgent != testCase.userAgent { t.Errorf("unexpected user agent: %v", client.userAgent) } }) } } hcloud-go-1.17.0/hcloud/datacenter.go000066400000000000000000000065121356226054300174120ustar00rootroot00000000000000package hcloud import ( "context" "fmt" "net/url" "strconv" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Datacenter represents a datacenter in the Hetzner Cloud. type Datacenter struct { ID int Name string Description string Location *Location ServerTypes DatacenterServerTypes } // DatacenterServerTypes represents the server types available and supported in a datacenter. type DatacenterServerTypes struct { Supported []*ServerType Available []*ServerType } // DatacenterClient is a client for the datacenter API. type DatacenterClient struct { client *Client } // GetByID retrieves a datacenter by its ID. If the datacenter does not exist, nil is returned. func (c *DatacenterClient) GetByID(ctx context.Context, id int) (*Datacenter, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/datacenters/%d", id), nil) if err != nil { return nil, nil, err } var body schema.DatacenterGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, resp, err } return DatacenterFromSchema(body.Datacenter), resp, nil } // GetByName retrieves an datacenter by its name. If the datacenter does not exist, nil is returned. func (c *DatacenterClient) GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) { datacenters, response, err := c.List(ctx, DatacenterListOpts{Name: name}) if len(datacenters) == 0 { return nil, response, err } return datacenters[0], response, err } // Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it // retrieves a datacenter by its name. If the datacenter does not exist, nil is returned. func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // DatacenterListOpts specifies options for listing datacenters. type DatacenterListOpts struct { ListOpts Name string } func (l DatacenterListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } return vals } // List returns a list of datacenters for a specific page. func (c *DatacenterClient) List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) { path := "/datacenters?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.DatacenterListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } datacenters := make([]*Datacenter, 0, len(body.Datacenters)) for _, i := range body.Datacenters { datacenters = append(datacenters, DatacenterFromSchema(i)) } return datacenters, resp, nil } // All returns all datacenters. func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) { allDatacenters := []*Datacenter{} opts := DatacenterListOpts{} opts.PerPage = 50 _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page datacenters, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allDatacenters = append(allDatacenters, datacenters...) return resp, nil }) if err != nil { return nil, err } return allDatacenters, nil } hcloud-go-1.17.0/hcloud/datacenter_test.go000066400000000000000000000121461356226054300204510ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestDatacenterClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.DatacenterGetResponse{ Datacenter: schema.Datacenter{ ID: 1, }, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } t.Run("via Get", func(t *testing.T) { datacenter, _, err := env.Client.Datacenter.Get(ctx, "1") if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if datacenter != nil { t.Fatal("expected no datacenter") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.DatacenterListResponse{ Datacenters: []schema.Datacenter{ { ID: 1, }, }, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } t.Run("via Get", func(t *testing.T) { datacenter, _, err := env.Client.Datacenter.Get(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if datacenter == nil { t.Fatal("no datacenter") } if datacenter.ID != 1 { t.Errorf("unexpected datacenter ID: %v", datacenter.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.DatacenterListResponse{ Datacenters: []schema.Datacenter{}, }) }) ctx := context.Background() datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if datacenter != nil { t.Fatal("unexpected datacenter") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } if name := r.URL.Query().Get("name"); name != "nbg1-dc3" { t.Errorf("expected name nbg1-dc3; got %q", name) } json.NewEncoder(w).Encode(schema.DatacenterListResponse{ Datacenters: []schema.Datacenter{ {ID: 1}, {ID: 2}, }, }) }) opts := DatacenterListOpts{} opts.Page = 2 opts.PerPage = 50 opts.Name = "nbg1-dc3" ctx := context.Background() datacenters, _, err := env.Client.Datacenter.List(ctx, opts) if err != nil { t.Fatal(err) } if len(datacenters) != 2 { t.Fatal("expected 2 datacenters") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Datacenters []schema.Datacenter `json:"datacenters"` Meta schema.Meta `json:"meta"` }{ Datacenters: []schema.Datacenter{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() datacenters, err := env.Client.Datacenter.All(ctx) if err != nil { t.Fatalf("Datacenter.List failed: %s", err) } if len(datacenters) != 3 { t.Fatalf("expected 3 datacenters; got %d", len(datacenters)) } if datacenters[0].ID != 1 || datacenters[1].ID != 2 || datacenters[2].ID != 3 { t.Errorf("unexpected datacenters") } }) } hcloud-go-1.17.0/hcloud/error.go000066400000000000000000000054401356226054300164300ustar00rootroot00000000000000package hcloud import "fmt" // ErrorCode represents an error code returned from the API. type ErrorCode string // Error codes returned from the API. const ( ErrorCodeServiceError ErrorCode = "service_error" // Generic service error ErrorCodeRateLimitExceeded ErrorCode = "rate_limit_exceeded" // Rate limit exceeded ErrorCodeUnknownError ErrorCode = "unknown_error" // Unknown error ErrorCodeNotFound ErrorCode = "not_found" // Resource not found ErrorCodeInvalidInput ErrorCode = "invalid_input" // Validation error ErrorCodeForbidden ErrorCode = "forbidden" // Insufficient permissions ErrorCodeJSONError ErrorCode = "json_error" // Invalid JSON in request ErrorCodeLocked ErrorCode = "locked" // Item is locked (Another action is running) ErrorCodeResourceLimitExceeded ErrorCode = "resource_limit_exceeded" // Resource limit exceeded ErrorCodeResourceUnavailable ErrorCode = "resource_unavailable" // Resource currently unavailable ErrorCodeUniquenessError ErrorCode = "uniqueness_error" // One or more fields must be unique ErrorCodeProtected ErrorCode = "protected" // The actions you are trying is protected ErrorCodeMaintenance ErrorCode = "maintenance" // Cannot perform operation due to maintenance ErrorCodeConflict ErrorCode = "conflict" // The resource has changed during the request, please retry ErrorCodeServerAlreadyAttached ErrorCode = "server_already_attached" // The server is already attached to the resource // Deprecated error codes // The actual value of this error code is limit_reached. The new error code // rate_limit_exceeded for ratelimiting was introduced before Hetzner Cloud // launched into the public. To make clients using the old error code still // work as expected, we set the value of the old error code to that of the // new error code. ErrorCodeLimitReached = ErrorCodeRateLimitExceeded ) // Error is an error returned from the API. type Error struct { Code ErrorCode Message string Details interface{} } func (e Error) Error() string { return fmt.Sprintf("%s (%s)", e.Message, e.Code) } // ErrorDetailsInvalidInput contains the details of an 'invalid_input' error. type ErrorDetailsInvalidInput struct { Fields []ErrorDetailsInvalidInputField } // ErrorDetailsInvalidInputField contains the validation errors reported on a field. type ErrorDetailsInvalidInputField struct { Name string Messages []string } // IsError returns whether err is an API error with the given error code. func IsError(err error, code ErrorCode) bool { apiErr, ok := err.(Error) return ok && apiErr.Code == code } hcloud-go-1.17.0/hcloud/floating_ip.go000066400000000000000000000250511356226054300175720ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // FloatingIP represents a Floating IP in the Hetzner Cloud. type FloatingIP struct { ID int Description string Created time.Time IP net.IP Network *net.IPNet Type FloatingIPType Server *Server DNSPtr map[string]string HomeLocation *Location Blocked bool Protection FloatingIPProtection Labels map[string]string Name string } // DNSPtrForIP returns the reverse DNS pointer of the IP address. func (f *FloatingIP) DNSPtrForIP(ip net.IP) string { return f.DNSPtr[ip.String()] } // FloatingIPProtection represents the protection level of a Floating IP. type FloatingIPProtection struct { Delete bool } // FloatingIPType represents the type of a Floating IP. type FloatingIPType string // Floating IP types. const ( FloatingIPTypeIPv4 FloatingIPType = "ipv4" FloatingIPTypeIPv6 FloatingIPType = "ipv6" ) // FloatingIPClient is a client for the Floating IP API. type FloatingIPClient struct { client *Client } // GetByID retrieves a Floating IP by its ID. If the Floating IP does not exist, // nil is returned. func (c *FloatingIPClient) GetByID(ctx context.Context, id int) (*FloatingIP, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/floating_ips/%d", id), nil) if err != nil { return nil, nil, err } var body schema.FloatingIPGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, resp, err } return FloatingIPFromSchema(body.FloatingIP), resp, nil } // GetByName retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned. func (c *FloatingIPClient) GetByName(ctx context.Context, name string) (*FloatingIP, *Response, error) { floatingIPs, response, err := c.List(ctx, FloatingIPListOpts{Name: name}) if len(floatingIPs) == 0 { return nil, response, err } return floatingIPs[0], response, err } // Get retrieves a Floating IP by its ID if the input can be parsed as an integer, otherwise it // retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned. func (c *FloatingIPClient) Get(ctx context.Context, idOrName string) (*FloatingIP, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // FloatingIPListOpts specifies options for listing Floating IPs. type FloatingIPListOpts struct { ListOpts Name string } func (l FloatingIPListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } return vals } // List returns a list of Floating IPs for a specific page. func (c *FloatingIPClient) List(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, *Response, error) { path := "/floating_ips?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.FloatingIPListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } floatingIPs := make([]*FloatingIP, 0, len(body.FloatingIPs)) for _, s := range body.FloatingIPs { floatingIPs = append(floatingIPs, FloatingIPFromSchema(s)) } return floatingIPs, resp, nil } // All returns all Floating IPs. func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) { return c.AllWithOpts(ctx, FloatingIPListOpts{ListOpts: ListOpts{PerPage: 50}}) } // AllWithOpts returns all Floating IPs for the given options. func (c *FloatingIPClient) AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) { allFloatingIPs := []*FloatingIP{} _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page floatingIPs, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allFloatingIPs = append(allFloatingIPs, floatingIPs...) return resp, nil }) if err != nil { return nil, err } return allFloatingIPs, nil } // FloatingIPCreateOpts specifies options for creating a Floating IP. type FloatingIPCreateOpts struct { Type FloatingIPType HomeLocation *Location Server *Server Description *string Name *string Labels map[string]string } // Validate checks if options are valid. func (o FloatingIPCreateOpts) Validate() error { switch o.Type { case FloatingIPTypeIPv4, FloatingIPTypeIPv6: break default: return errors.New("missing or invalid type") } if o.HomeLocation == nil && o.Server == nil { return errors.New("one of home location or server is required") } return nil } // FloatingIPCreateResult is the result of creating a Floating IP. type FloatingIPCreateResult struct { FloatingIP *FloatingIP Action *Action } // Create creates a Floating IP. func (c *FloatingIPClient) Create(ctx context.Context, opts FloatingIPCreateOpts) (FloatingIPCreateResult, *Response, error) { if err := opts.Validate(); err != nil { return FloatingIPCreateResult{}, nil, err } reqBody := schema.FloatingIPCreateRequest{ Type: string(opts.Type), Description: opts.Description, Name: opts.Name, } if opts.HomeLocation != nil { reqBody.HomeLocation = String(opts.HomeLocation.Name) } if opts.Server != nil { reqBody.Server = Int(opts.Server.ID) } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return FloatingIPCreateResult{}, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/floating_ips", bytes.NewReader(reqBodyData)) if err != nil { return FloatingIPCreateResult{}, nil, err } var respBody schema.FloatingIPCreateResponse resp, err := c.client.Do(req, &respBody) if err != nil { return FloatingIPCreateResult{}, resp, err } var action *Action if respBody.Action != nil { action = ActionFromSchema(*respBody.Action) } return FloatingIPCreateResult{ FloatingIP: FloatingIPFromSchema(respBody.FloatingIP), Action: action, }, resp, nil } // Delete deletes a Floating IP. func (c *FloatingIPClient) Delete(ctx context.Context, floatingIP *FloatingIP) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/floating_ips/%d", floatingIP.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } // FloatingIPUpdateOpts specifies options for updating a Floating IP. type FloatingIPUpdateOpts struct { Description string Labels map[string]string Name string } // Update updates a Floating IP. func (c *FloatingIPClient) Update(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPUpdateOpts) (*FloatingIP, *Response, error) { reqBody := schema.FloatingIPUpdateRequest{ Description: opts.Description, Name: opts.Name, } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/floating_ips/%d", floatingIP.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.FloatingIPUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return FloatingIPFromSchema(respBody.FloatingIP), resp, nil } // Assign assigns a Floating IP to a server. func (c *FloatingIPClient) Assign(ctx context.Context, floatingIP *FloatingIP, server *Server) (*Action, *Response, error) { reqBody := schema.FloatingIPActionAssignRequest{ Server: server.ID, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/floating_ips/%d/actions/assign", floatingIP.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.FloatingIPActionAssignResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // Unassign unassigns a Floating IP from the currently assigned server. func (c *FloatingIPClient) Unassign(ctx context.Context, floatingIP *FloatingIP) (*Action, *Response, error) { var reqBody schema.FloatingIPActionUnassignRequest reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/floating_ips/%d/actions/unassign", floatingIP.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.FloatingIPActionUnassignResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // ChangeDNSPtr changes or resets the reverse DNS pointer for a Floating IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (c *FloatingIPClient) ChangeDNSPtr(ctx context.Context, floatingIP *FloatingIP, ip string, ptr *string) (*Action, *Response, error) { reqBody := schema.FloatingIPActionChangeDNSPtrRequest{ IP: ip, DNSPtr: ptr, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", floatingIP.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.FloatingIPActionChangeDNSPtrResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // FloatingIPChangeProtectionOpts specifies options for changing the resource protection level of a Floating IP. type FloatingIPChangeProtectionOpts struct { Delete *bool } // ChangeProtection changes the resource protection level of a Floating IP. func (c *FloatingIPClient) ChangeProtection(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPChangeProtectionOpts) (*Action, *Response, error) { reqBody := schema.FloatingIPActionChangeProtectionRequest{ Delete: opts.Delete, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/floating_ips/%d/actions/change_protection", floatingIP.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.FloatingIPActionChangeProtectionResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } hcloud-go-1.17.0/hcloud/floating_ip_test.go000066400000000000000000000376051356226054300206410ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestFloatingIPClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.FloatingIPGetResponse{ FloatingIP: schema.FloatingIP{ ID: 1, Type: "ipv4", IP: "131.232.99.1", }, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected ID: %v", floatingIP.ID) } t.Run("via Get", func(t *testing.T) { floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "1") if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", floatingIP.ID) } }) } func TestFloatingIPClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if floatingIP != nil { t.Fatal("expected no Floating IP") } } func TestFloatingIPClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myFloatingIP" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{ { ID: 1, Name: "myFloatingIP", Type: "ipv4", IP: "131.232.99.1", }, }, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByName(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected ID: %v", floatingIP.ID) } t.Run("via Get", func(t *testing.T) { floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP == nil { t.Fatal("no Floating IP") } if floatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", floatingIP.ID) } }) } func TestFloatingIPClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myFloatingIP" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{}, }) }) ctx := context.Background() floatingIP, _, err := env.Client.FloatingIP.GetByName(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP != nil { t.Fatal("expected no Floating IP") } t.Run("via Get", func(t *testing.T) { floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "myFloatingIP") if err != nil { t.Fatal(err) } if floatingIP != nil { t.Fatal("expected no Floating IP") } }) } func TestFloatingIPClientList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{ {ID: 1, Type: "ipv4", IP: "131.232.99.1"}, {ID: 2, Type: "ipv4", IP: "131.232.99.1"}, }, }) }) opts := FloatingIPListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() floatingIPs, _, err := env.Client.FloatingIP.List(ctx, opts) if err != nil { t.Fatal(err) } if len(floatingIPs) != 2 { t.Fatal("expected 2 Floating IPs") } } func TestFloatingIPClientAllWithOpts(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } json.NewEncoder(w).Encode(schema.FloatingIPListResponse{ FloatingIPs: []schema.FloatingIP{ {ID: 1, Type: "ipv4", IP: "131.232.99.1"}, {ID: 2, Type: "ipv4", IP: "131.232.99.1"}, }, }) }) ctx := context.Background() opts := FloatingIPListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}} floatingIPs, err := env.Client.FloatingIP.AllWithOpts(ctx, opts) if err != nil { t.Fatal(err) } if len(floatingIPs) != 2 { t.Fatal("expected 2 Floating IPs") } } func TestFloatingIPClientCreate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FloatingIPCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.FloatingIPCreateResponse{ FloatingIP: schema.FloatingIP{ID: 1, Type: "ipv4", IP: "131.232.99.1"}, Action: &schema.Action{ ID: 1, }, }) }) opts := FloatingIPCreateOpts{ Type: FloatingIPTypeIPv4, Description: String("test"), HomeLocation: &Location{Name: "test"}, Server: &Server{ID: 1}, Labels: map[string]string{"key": "value"}, } ctx := context.Background() result, _, err := env.Client.FloatingIP.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.FloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %d", result.FloatingIP.ID) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } } func TestFloatingIPClientCreateWithName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FloatingIPCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name == nil || *reqBody.Name != "MyFloatingIP" { t.Errorf("unexpected name in request: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.FloatingIPCreateResponse{ FloatingIP: schema.FloatingIP{ID: 1, Type: "ipv4", IP: "131.232.99.1"}, Action: &schema.Action{ ID: 1, }, }) }) opts := FloatingIPCreateOpts{ Type: FloatingIPTypeIPv4, Description: String("test"), HomeLocation: &Location{Name: "test"}, Server: &Server{ID: 1}, Name: String("MyFloatingIP"), Labels: map[string]string{"key": "value"}, } ctx := context.Background() result, _, err := env.Client.FloatingIP.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.FloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %d", result.FloatingIP.ID) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } } func TestFloatingIPClientDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { return }) var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) _, err := env.Client.FloatingIP.Delete(ctx, floatingIP) if err != nil { t.Fatal(err) } } func TestFloatingIPClientUpdate(t *testing.T) { var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) t.Run("update description", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description != "test" { t.Errorf("unexpected description: %v", reqBody.Description) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{ Description: "test", } updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{ Labels: map[string]string{"key": "value"}, } updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{ Name: "test", } updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.FloatingIPUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description != "" { t.Errorf("unexpected no description, but got: %v", reqBody.Description) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{ FloatingIP: schema.FloatingIP{ ID: 1, }, }) }) opts := FloatingIPUpdateOpts{} updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if updatedFloatingIP.ID != 1 { t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID) } }) } func TestFloatingIPClientAssign(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/assign", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.FloatingIPActionAssignRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Server != 1 { t.Errorf("unexpected server ID: %d", reqBody.Server) } json.NewEncoder(w).Encode(schema.FloatingIPActionAssignResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} server = &Server{ID: 1} ) action, _, err := env.Client.FloatingIP.Assign(ctx, floatingIP, server) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestFloatingIPClientUnassign(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/unassign", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.FloatingIPActionAssignResponse{ Action: schema.Action{ ID: 1, }, }) }) var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) action, _, err := env.Client.FloatingIP.Unassign(ctx, floatingIP) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestFloatingIPClientChangeDNSPtr(t *testing.T) { var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) t.Run("set", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.FloatingIPActionChangeDNSPtrRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.IP != "127.0.0.1" { t.Errorf("unexpected IP: %v", reqBody.IP) } if reqBody.DNSPtr == nil || *reqBody.DNSPtr != "example.com" { t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr) } json.NewEncoder(w).Encode(schema.FloatingIPActionChangeDNSPtrResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, "127.0.0.1", String("example.com")) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("reset", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.FloatingIPActionChangeDNSPtrRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.IP != "127.0.0.1" { t.Errorf("unexpected IP: %v", reqBody.IP) } if reqBody.DNSPtr != nil { t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr) } json.NewEncoder(w).Encode(schema.FloatingIPActionChangeDNSPtrResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, "127.0.0.1", nil) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestFloatingIPClientChangeProtection(t *testing.T) { var ( ctx = context.Background() floatingIP = &FloatingIP{ID: 1} ) t.Run("enable delete protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/floating_ips/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.FloatingIPActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } json.NewEncoder(w).Encode(schema.FloatingIPActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := FloatingIPChangeProtectionOpts{ Delete: Bool(true), } action, _, err := env.Client.FloatingIP.ChangeProtection(ctx, floatingIP, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } hcloud-go-1.17.0/hcloud/hcloud.go000066400000000000000000000002461356226054300165540ustar00rootroot00000000000000// Package hcloud is a library for the Hetzner Cloud API. package hcloud // Version is the library's version following Semantic Versioning. const Version = "1.17.0" hcloud-go-1.17.0/hcloud/hcloud_test.go000066400000000000000000000006651356226054300176200ustar00rootroot00000000000000package hcloud_test import ( "context" "fmt" "log" "github.com/hetznercloud/hcloud-go/hcloud" ) func Example() { client := hcloud.NewClient(hcloud.WithToken("token")) server, _, err := client.Server.GetByID(context.Background(), 1) if err != nil { log.Fatalf("error retrieving server: %s\n", err) } if server != nil { fmt.Printf("server 1 is called %q\n", server.Name) } else { fmt.Println("server 1 not found") } } hcloud-go-1.17.0/hcloud/helper.go000066400000000000000000000004351356226054300165550ustar00rootroot00000000000000package hcloud // String returns a pointer to the passed string s. func String(s string) *string { return &s } // Int returns a pointer to the passed integer i. func Int(i int) *int { return &i } // Bool returns a pointer to the passed bool b. func Bool(b bool) *bool { return &b } hcloud-go-1.17.0/hcloud/image.go000066400000000000000000000153271356226054300163660ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "fmt" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Image represents an Image in the Hetzner Cloud. type Image struct { ID int Name string Type ImageType Status ImageStatus Description string ImageSize float32 DiskSize float32 Created time.Time CreatedFrom *Server BoundTo *Server RapidDeploy bool OSFlavor string OSVersion string Protection ImageProtection Deprecated time.Time // The zero value denotes the image is not deprecated. Labels map[string]string } // IsDeprecated returns whether the image is deprecated. func (image *Image) IsDeprecated() bool { return !image.Deprecated.IsZero() } // ImageProtection represents the protection level of an image. type ImageProtection struct { Delete bool } // ImageType specifies the type of an image. type ImageType string const ( // ImageTypeSnapshot represents a snapshot image. ImageTypeSnapshot ImageType = "snapshot" // ImageTypeBackup represents a backup image. ImageTypeBackup ImageType = "backup" // ImageTypeSystem represents a system image. ImageTypeSystem ImageType = "system" ) // ImageStatus specifies the status of an image. type ImageStatus string const ( // ImageStatusCreating is the status when an image is being created. ImageStatusCreating ImageStatus = "creating" // ImageStatusAvailable is the status when an image is available. ImageStatusAvailable ImageStatus = "available" ) // ImageClient is a client for the image API. type ImageClient struct { client *Client } // GetByID retrieves an image by its ID. If the image does not exist, nil is returned. func (c *ImageClient) GetByID(ctx context.Context, id int) (*Image, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/images/%d", id), nil) if err != nil { return nil, nil, err } var body schema.ImageGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return ImageFromSchema(body.Image), resp, nil } // GetByName retrieves an image by its name. If the image does not exist, nil is returned. func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Response, error) { images, response, err := c.List(ctx, ImageListOpts{Name: name}) if len(images) == 0 { return nil, response, err } return images[0], response, err } // Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it // retrieves an image by its name. If the image does not exist, nil is returned. func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // ImageListOpts specifies options for listing images. type ImageListOpts struct { ListOpts Type []ImageType BoundTo *Server Name string Sort []string Status []ImageStatus } func (l ImageListOpts) values() url.Values { vals := l.ListOpts.values() for _, typ := range l.Type { vals.Add("type", string(typ)) } if l.BoundTo != nil { vals.Add("bound_to", strconv.Itoa(l.BoundTo.ID)) } if l.Name != "" { vals.Add("name", l.Name) } for _, sort := range l.Sort { vals.Add("sort", sort) } for _, status := range l.Status { vals.Add("status", string(status)) } return vals } // List returns a list of images for a specific page. func (c *ImageClient) List(ctx context.Context, opts ImageListOpts) ([]*Image, *Response, error) { path := "/images?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.ImageListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } images := make([]*Image, 0, len(body.Images)) for _, i := range body.Images { images = append(images, ImageFromSchema(i)) } return images, resp, nil } // All returns all images. func (c *ImageClient) All(ctx context.Context) ([]*Image, error) { return c.AllWithOpts(ctx, ImageListOpts{ListOpts: ListOpts{PerPage: 50}}) } // AllWithOpts returns all images for the given options. func (c *ImageClient) AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) { allImages := []*Image{} _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page images, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allImages = append(allImages, images...) return resp, nil }) if err != nil { return nil, err } return allImages, nil } // Delete deletes an image. func (c *ImageClient) Delete(ctx context.Context, image *Image) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/images/%d", image.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } // ImageUpdateOpts specifies options for updating an image. type ImageUpdateOpts struct { Description *string Type ImageType Labels map[string]string } // Update updates an image. func (c *ImageClient) Update(ctx context.Context, image *Image, opts ImageUpdateOpts) (*Image, *Response, error) { reqBody := schema.ImageUpdateRequest{ Description: opts.Description, } if opts.Type != "" { reqBody.Type = String(string(opts.Type)) } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/images/%d", image.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ImageUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ImageFromSchema(respBody.Image), resp, nil } // ImageChangeProtectionOpts specifies options for changing the resource protection level of an image. type ImageChangeProtectionOpts struct { Delete *bool } // ChangeProtection changes the resource protection level of an image. func (c *ImageClient) ChangeProtection(ctx context.Context, image *Image, opts ImageChangeProtectionOpts) (*Action, *Response, error) { reqBody := schema.ImageActionChangeProtectionRequest{ Delete: opts.Delete, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/images/%d/actions/change_protection", image.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ImageActionChangeProtectionResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } hcloud-go-1.17.0/hcloud/image_test.go000066400000000000000000000232701356226054300174210ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestImageIsDeprecated(t *testing.T) { t.Run("not deprecated", func(t *testing.T) { image := &Image{} if image.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", image.IsDeprecated()) } }) t.Run("deprecated", func(t *testing.T) { image := &Image{ Deprecated: time.Now(), } if !image.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", image.IsDeprecated()) } }) } func TestImageClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ImageGetResponse{ Image: schema.Image{ ID: 1, }, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } t.Run("via Get", func(t *testing.T) { image, _, err := env.Client.Image.Get(ctx, "1") if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if image != nil { t.Fatal("expected no image") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my+image" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ImageListResponse{ Images: []schema.Image{ { ID: 1, }, }, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByName(ctx, "my image") if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } t.Run("via Get", func(t *testing.T) { image, _, err := env.Client.Image.Get(ctx, "my image") if err != nil { t.Fatal(err) } if image == nil { t.Fatal("no image") } if image.ID != 1 { t.Errorf("unexpected image ID: %v", image.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my+image" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ImageListResponse{ Images: []schema.Image{}, }) }) ctx := context.Background() image, _, err := env.Client.Image.GetByName(ctx, "my image") if err != nil { t.Fatal(err) } if image != nil { t.Fatal("unexpected image") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ImageListResponse{ Images: []schema.Image{ {ID: 1}, {ID: 2}, }, }) }) opts := ImageListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() images, _, err := env.Client.Image.List(ctx, opts) if err != nil { t.Fatal(err) } if len(images) != 2 { t.Fatal("expected 2 images") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Images []schema.Image `json:"images"` Meta schema.Meta `json:"meta"` }{ Images: []schema.Image{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() images, err := env.Client.Image.All(ctx) if err != nil { t.Fatalf("Image.List failed: %s", err) } if len(images) != 3 { t.Fatalf("expected 3 images; got %d", len(images)) } if images[0].ID != 1 || images[1].ID != 2 || images[2].ID != 3 { t.Errorf("unexpected images") } }) t.Run("AllWithOpts", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } if name := r.URL.Query().Get("name"); name != "my-image" { t.Errorf("unexpected name: %s", name) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Images []schema.Image `json:"images"` Meta schema.Meta `json:"meta"` }{ Images: []schema.Image{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() opts := ImageListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}, Name: "my-image", Type: []ImageType{"backup", "system"}} images, err := env.Client.Image.AllWithOpts(ctx, opts) if err != nil { t.Fatal(err) } if len(images) != 3 { t.Fatalf("expected 3 images; got %d", len(images)) } if images[0].ID != 1 || images[1].ID != 2 || images[2].ID != 3 { t.Errorf("unexpected images") } }) t.Run("Delete", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { return }) var ( ctx = context.Background() image = &Image{ID: 1} ) _, err := env.Client.Image.Delete(ctx, image) if err != nil { t.Fatalf("Image.Delete failed: %s", err) } }) } func TestImageClientUpdate(t *testing.T) { var ( ctx = context.Background() image = &Image{ID: 1} ) t.Run("description and type", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ImageUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description == nil || *reqBody.Description != "test" { t.Errorf("unexpected description: %v", reqBody.Description) } if reqBody.Type == nil || *reqBody.Type != "snapshot" { t.Errorf("unexpected type: %v", reqBody.Type) } json.NewEncoder(w).Encode(schema.ImageUpdateResponse{ Image: schema.Image{ ID: 1, }, }) }) opts := ImageUpdateOpts{ Description: String("test"), Type: ImageTypeSnapshot, } updatedImage, _, err := env.Client.Image.Update(ctx, image, opts) if err != nil { t.Fatal(err) } if updatedImage.ID != 1 { t.Errorf("unexpected image ID: %v", updatedImage.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ImageUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Description != nil { t.Errorf("unexpected no description, but got: %v", reqBody.Description) } if reqBody.Type != nil { t.Errorf("unexpected no type, but got: %v", reqBody.Type) } json.NewEncoder(w).Encode(schema.ImageUpdateResponse{ Image: schema.Image{ ID: 1, }, }) }) opts := ImageUpdateOpts{} updatedImage, _, err := env.Client.Image.Update(ctx, image, opts) if err != nil { t.Fatal(err) } if updatedImage.ID != 1 { t.Errorf("unexpected image ID: %v", updatedImage.ID) } }) } func TestImageClientChangeProtection(t *testing.T) { var ( ctx = context.Background() image = &Image{ID: 1} ) t.Run("enable delete protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/images/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ImageActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } json.NewEncoder(w).Encode(schema.ImageActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ImageChangeProtectionOpts{ Delete: Bool(true), } action, _, err := env.Client.Image.ChangeProtection(ctx, image, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } hcloud-go-1.17.0/hcloud/iso.go000066400000000000000000000060121356226054300160650ustar00rootroot00000000000000package hcloud import ( "context" "fmt" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // ISO represents an ISO image in the Hetzner Cloud. type ISO struct { ID int Name string Description string Type ISOType Deprecated time.Time } // IsDeprecated returns true if the ISO is deprecated func (iso *ISO) IsDeprecated() bool { return !iso.Deprecated.IsZero() } // ISOType specifies the type of an ISO image. type ISOType string const ( // ISOTypePublic is the type of a public ISO image. ISOTypePublic ISOType = "public" // ISOTypePrivate is the type of a private ISO image. ISOTypePrivate ISOType = "private" ) // ISOClient is a client for the ISO API. type ISOClient struct { client *Client } // GetByID retrieves an ISO by its ID. func (c *ISOClient) GetByID(ctx context.Context, id int) (*ISO, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/isos/%d", id), nil) if err != nil { return nil, nil, err } var body schema.ISOGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, resp, err } return ISOFromSchema(body.ISO), resp, nil } // GetByName retrieves an ISO by its name. func (c *ISOClient) GetByName(ctx context.Context, name string) (*ISO, *Response, error) { isos, response, err := c.List(ctx, ISOListOpts{Name: name}) if len(isos) == 0 { return nil, response, err } return isos[0], response, err } // Get retrieves an ISO by its ID if the input can be parsed as an integer, otherwise it retrieves an ISO by its name. func (c *ISOClient) Get(ctx context.Context, idOrName string) (*ISO, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // ISOListOpts specifies options for listing isos. type ISOListOpts struct { ListOpts Name string } func (l ISOListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } return vals } // List returns a list of ISOs for a specific page. func (c *ISOClient) List(ctx context.Context, opts ISOListOpts) ([]*ISO, *Response, error) { path := "/isos?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.ISOListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } isos := make([]*ISO, 0, len(body.ISOs)) for _, i := range body.ISOs { isos = append(isos, ISOFromSchema(i)) } return isos, resp, nil } // All returns all ISOs. func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) { allISOs := []*ISO{} opts := ISOListOpts{} opts.PerPage = 50 _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page isos, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allISOs = append(allISOs, isos...) return resp, nil }) if err != nil { return nil, err } return allISOs, nil } hcloud-go-1.17.0/hcloud/iso_test.go000066400000000000000000000115341356226054300171310ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestISOIsDeprecated(t *testing.T) { t.Run("not deprecated", func(t *testing.T) { iso := &ISO{} if iso.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", iso.IsDeprecated()) } }) t.Run("deprecated", func(t *testing.T) { iso := &ISO{ Deprecated: time.Now(), } if !iso.IsDeprecated() { t.Errorf("unexpected value for IsDeprecated: %v", iso.IsDeprecated()) } }) } func TestISOClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ISOGetResponse{ ISO: schema.ISO{ ID: 1, }, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } t.Run("via Get", func(t *testing.T) { iso, _, err := env.Client.ISO.Get(ctx, "1") if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if iso != nil { t.Fatal("expected no iso") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=debian-9" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ISOListResponse{ ISOs: []schema.ISO{ { ID: 1, }, }, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByName(ctx, "debian-9") if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } t.Run("via Get", func(t *testing.T) { iso, _, err := env.Client.ISO.Get(ctx, "debian-9") if err != nil { t.Fatal(err) } if iso == nil { t.Fatal("no iso") } if iso.ID != 1 { t.Errorf("unexpected iso ID: %v", iso.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=debian-9" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ISOListResponse{ ISOs: []schema.ISO{}, }) }) ctx := context.Background() iso, _, err := env.Client.ISO.GetByName(ctx, "debian-9") if err != nil { t.Fatal(err) } if iso != nil { t.Fatal("unexpected iso") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ISOListResponse{ ISOs: []schema.ISO{ {ID: 1}, {ID: 2}, }, }) }) opts := ISOListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() isos, _, err := env.Client.ISO.List(ctx, opts) if err != nil { t.Fatal(err) } if len(isos) != 2 { t.Fatal("expected 2 isos") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { ISOs []schema.ISO `json:"isos"` Meta schema.Meta `json:"meta"` }{ ISOs: []schema.ISO{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() isos, err := env.Client.ISO.All(ctx) if err != nil { t.Fatalf("ISO.List failed: %s", err) } if len(isos) != 3 { t.Fatalf("expected 3 isos; got %d", len(isos)) } if isos[0].ID != 1 || isos[1].ID != 2 || isos[2].ID != 3 { t.Errorf("unexpected isos") } }) } hcloud-go-1.17.0/hcloud/location.go000066400000000000000000000061231356226054300171060ustar00rootroot00000000000000package hcloud import ( "context" "fmt" "net/url" "strconv" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Location represents a location in the Hetzner Cloud. type Location struct { ID int Name string Description string Country string City string Latitude float64 Longitude float64 NetworkZone NetworkZone } // LocationClient is a client for the location API. type LocationClient struct { client *Client } // GetByID retrieves a location by its ID. If the location does not exist, nil is returned. func (c *LocationClient) GetByID(ctx context.Context, id int) (*Location, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/locations/%d", id), nil) if err != nil { return nil, nil, err } var body schema.LocationGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, resp, err } return LocationFromSchema(body.Location), resp, nil } // GetByName retrieves an location by its name. If the location does not exist, nil is returned. func (c *LocationClient) GetByName(ctx context.Context, name string) (*Location, *Response, error) { locations, response, err := c.List(ctx, LocationListOpts{Name: name}) if len(locations) == 0 { return nil, response, err } return locations[0], response, err } // Get retrieves a location by its ID if the input can be parsed as an integer, otherwise it // retrieves a location by its name. If the location does not exist, nil is returned. func (c *LocationClient) Get(ctx context.Context, idOrName string) (*Location, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // LocationListOpts specifies options for listing location. type LocationListOpts struct { ListOpts Name string } func (l LocationListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } return vals } // List returns a list of locations for a specific page. func (c *LocationClient) List(ctx context.Context, opts LocationListOpts) ([]*Location, *Response, error) { path := "/locations?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.LocationListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } locations := make([]*Location, 0, len(body.Locations)) for _, i := range body.Locations { locations = append(locations, LocationFromSchema(i)) } return locations, resp, nil } // All returns all locations. func (c *LocationClient) All(ctx context.Context) ([]*Location, error) { allLocations := []*Location{} opts := LocationListOpts{} opts.PerPage = 50 _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page locations, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allLocations = append(allLocations, locations...) return resp, nil }) if err != nil { return nil, err } return allLocations, nil } hcloud-go-1.17.0/hcloud/location_test.go000066400000000000000000000117001356226054300201420ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestLocationClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.LocationGetResponse{ Location: schema.Location{ ID: 1, }, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } t.Run("via Get", func(t *testing.T) { location, _, err := env.Client.Location.Get(ctx, "1") if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if location != nil { t.Fatal("expected no location") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LocationListResponse{ Locations: []schema.Location{ { ID: 1, }, }, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } t.Run("via Get", func(t *testing.T) { location, _, err := env.Client.Location.Get(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if location == nil { t.Fatal("no location") } if location.ID != 1 { t.Errorf("unexpected location ID: %v", location.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=fsn1-dc8" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.LocationListResponse{ Locations: []schema.Location{}, }) }) ctx := context.Background() location, _, err := env.Client.Location.GetByName(ctx, "fsn1-dc8") if err != nil { t.Fatal(err) } if location != nil { t.Fatal("unexpected location") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } if name := r.URL.Query().Get("name"); name != "fsn1" { t.Errorf("expected name fsn1; got %q", name) } json.NewEncoder(w).Encode(schema.LocationListResponse{ Locations: []schema.Location{ {ID: 1}, {ID: 2}, }, }) }) opts := LocationListOpts{} opts.Page = 2 opts.PerPage = 50 opts.Name = "fsn1" ctx := context.Background() locations, _, err := env.Client.Location.List(ctx, opts) if err != nil { t.Fatal(err) } if len(locations) != 2 { t.Fatal("expected 2 locations") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Locations []schema.Location `json:"locations"` Meta schema.Meta `json:"meta"` }{ Locations: []schema.Location{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() locations, err := env.Client.Location.All(ctx) if err != nil { t.Fatalf("Location.List failed: %s", err) } if len(locations) != 3 { t.Fatalf("expected 3 locations; got %d", len(locations)) } if locations[0].ID != 1 || locations[1].ID != 2 || locations[2].ID != 3 { t.Errorf("unexpected locations") } }) } hcloud-go-1.17.0/hcloud/network.go000066400000000000000000000305651356226054300167760ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // NetworkZone specifies a network zone. type NetworkZone string // List of available Network Zones. const ( NetworkZoneEUCentral NetworkZone = "eu-central" ) // NetworkSubnetType specifies a type of a subnet. type NetworkSubnetType string // List of available network subnet types. const ( NetworkSubnetTypeServer NetworkSubnetType = "server" ) // Network represents a network in the Hetzner Cloud. type Network struct { ID int Name string Created time.Time IPRange *net.IPNet Subnets []NetworkSubnet Routes []NetworkRoute Servers []*Server Protection NetworkProtection Labels map[string]string } // NetworkSubnet represents a subnet of a network in the Hetzner Cloud. type NetworkSubnet struct { Type NetworkSubnetType IPRange *net.IPNet NetworkZone NetworkZone Gateway net.IP } // NetworkRoute represents a route of a network. type NetworkRoute struct { Destination *net.IPNet Gateway net.IP } // NetworkProtection represents the protection level of a network. type NetworkProtection struct { Delete bool } // NetworkClient is a client for the network API. type NetworkClient struct { client *Client } // GetByID retrieves a network by its ID. If the network does not exist, nil is returned. func (c *NetworkClient) GetByID(ctx context.Context, id int) (*Network, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/networks/%d", id), nil) if err != nil { return nil, nil, err } var body schema.NetworkGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return NetworkFromSchema(body.Network), resp, nil } // GetByName retrieves a network by its name. If the network does not exist, nil is returned. func (c *NetworkClient) GetByName(ctx context.Context, name string) (*Network, *Response, error) { Networks, response, err := c.List(ctx, NetworkListOpts{Name: name}) if len(Networks) == 0 { return nil, response, err } return Networks[0], response, err } // Get retrieves a network by its ID if the input can be parsed as an integer, otherwise it // retrieves a network by its name. If the network does not exist, nil is returned. func (c *NetworkClient) Get(ctx context.Context, idOrName string) (*Network, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // NetworkListOpts specifies options for listing networks. type NetworkListOpts struct { ListOpts Name string } func (l NetworkListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } return vals } // List returns a list of networks for a specific page. func (c *NetworkClient) List(ctx context.Context, opts NetworkListOpts) ([]*Network, *Response, error) { path := "/networks?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.NetworkListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } Networks := make([]*Network, 0, len(body.Networks)) for _, s := range body.Networks { Networks = append(Networks, NetworkFromSchema(s)) } return Networks, resp, nil } // All returns all networks. func (c *NetworkClient) All(ctx context.Context) ([]*Network, error) { return c.AllWithOpts(ctx, NetworkListOpts{ListOpts: ListOpts{PerPage: 50}}) } // AllWithOpts returns all networks for the given options. func (c *NetworkClient) AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) { var allNetworks []*Network _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page Networks, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allNetworks = append(allNetworks, Networks...) return resp, nil }) if err != nil { return nil, err } return allNetworks, nil } // Delete deletes a network. func (c *NetworkClient) Delete(ctx context.Context, network *Network) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/networks/%d", network.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } // NetworkUpdateOpts specifies options for updating a network. type NetworkUpdateOpts struct { Name string Labels map[string]string } // Update updates a network. func (c *NetworkClient) Update(ctx context.Context, network *Network, opts NetworkUpdateOpts) (*Network, *Response, error) { reqBody := schema.NetworkUpdateRequest{ Name: opts.Name, } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/networks/%d", network.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return NetworkFromSchema(respBody.Network), resp, nil } // NetworkCreateOpts specifies options for creating a new network. type NetworkCreateOpts struct { Name string IPRange *net.IPNet Subnets []NetworkSubnet Routes []NetworkRoute Labels map[string]string } // Validate checks if options are valid. func (o NetworkCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } if o.IPRange == nil || o.IPRange.String() == "" { return errors.New("missing IP range") } return nil } // Create creates a new network. func (c *NetworkClient) Create(ctx context.Context, opts NetworkCreateOpts) (*Network, *Response, error) { if err := opts.Validate(); err != nil { return nil, nil, err } reqBody := schema.NetworkCreateRequest{ Name: opts.Name, IPRange: opts.IPRange.String(), } for _, subnet := range opts.Subnets { reqBody.Subnets = append(reqBody.Subnets, schema.NetworkSubnet{ Type: string(subnet.Type), IPRange: subnet.IPRange.String(), NetworkZone: string(subnet.NetworkZone), }) } for _, route := range opts.Routes { reqBody.Routes = append(reqBody.Routes, schema.NetworkRoute{ Destination: route.Destination.String(), Gateway: route.Gateway.String(), }) } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/networks", bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkCreateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return NetworkFromSchema(respBody.Network), resp, nil } // NetworkChangeIPRangeOpts specifies options for changing the IP range of a network. type NetworkChangeIPRangeOpts struct { IPRange *net.IPNet } // ChangeIPRange changes the IP range of a network. func (c *NetworkClient) ChangeIPRange(ctx context.Context, network *Network, opts NetworkChangeIPRangeOpts) (*Action, *Response, error) { reqBody := schema.NetworkActionChangeIPRangeRequest{ IPRange: opts.IPRange.String(), } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/networks/%d/actions/change_ip_range", network.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkActionChangeIPRangeResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // NetworkAddSubnetOpts specifies options for adding a subnet to a network. type NetworkAddSubnetOpts struct { Subnet NetworkSubnet } // AddSubnet adds a subnet to a network. func (c *NetworkClient) AddSubnet(ctx context.Context, network *Network, opts NetworkAddSubnetOpts) (*Action, *Response, error) { reqBody := schema.NetworkActionAddSubnetRequest{ Type: string(opts.Subnet.Type), NetworkZone: string(opts.Subnet.NetworkZone), } if opts.Subnet.IPRange != nil { reqBody.IPRange = opts.Subnet.IPRange.String() } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/networks/%d/actions/add_subnet", network.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkActionAddSubnetResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // NetworkDeleteSubnetOpts specifies options for deleting a subnet from a network. type NetworkDeleteSubnetOpts struct { Subnet NetworkSubnet } // DeleteSubnet deletes a subnet from a network. func (c *NetworkClient) DeleteSubnet(ctx context.Context, network *Network, opts NetworkDeleteSubnetOpts) (*Action, *Response, error) { reqBody := schema.NetworkActionDeleteSubnetRequest{ IPRange: opts.Subnet.IPRange.String(), } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/networks/%d/actions/delete_subnet", network.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkActionDeleteSubnetResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // NetworkAddRouteOpts specifies options for adding a route to a network. type NetworkAddRouteOpts struct { Route NetworkRoute } // AddRoute adds a route to a network. func (c *NetworkClient) AddRoute(ctx context.Context, network *Network, opts NetworkAddRouteOpts) (*Action, *Response, error) { reqBody := schema.NetworkActionAddRouteRequest{ Destination: opts.Route.Destination.String(), Gateway: opts.Route.Gateway.String(), } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/networks/%d/actions/add_route", network.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkActionAddSubnetResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // NetworkDeleteRouteOpts specifies options for deleting a route from a network. type NetworkDeleteRouteOpts struct { Route NetworkRoute } // DeleteRoute deletes a route from a network. func (c *NetworkClient) DeleteRoute(ctx context.Context, network *Network, opts NetworkDeleteRouteOpts) (*Action, *Response, error) { reqBody := schema.NetworkActionDeleteRouteRequest{ Destination: opts.Route.Destination.String(), Gateway: opts.Route.Gateway.String(), } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/networks/%d/actions/delete_route", network.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkActionDeleteSubnetResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // NetworkChangeProtectionOpts specifies options for changing the resource protection level of a network. type NetworkChangeProtectionOpts struct { Delete *bool } // ChangeProtection changes the resource protection level of a network. func (c *NetworkClient) ChangeProtection(ctx context.Context, network *Network, opts NetworkChangeProtectionOpts) (*Action, *Response, error) { reqBody := schema.NetworkActionChangeProtectionRequest{ Delete: opts.Delete, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/networks/%d/actions/change_protection", network.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.NetworkActionChangeProtectionResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } hcloud-go-1.17.0/hcloud/network_test.go000066400000000000000000000334511356226054300200320ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestNetworkClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkGetResponse{ Network: schema.Network{ ID: 1, }, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } t.Run("called via Get", func(t *testing.T) { network, _, err := env.Client.Network.Get(ctx, "1") if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } }) } func TestNetworkClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if network != nil { t.Fatal("expected no network") } } func TestNetworkClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mynet" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.NetworkListResponse{ Networks: []schema.Network{ { ID: 1, Name: "mynet", }, }, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByName(ctx, "mynet") if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } t.Run("via Get", func(t *testing.T) { network, _, err := env.Client.Network.Get(ctx, "mynet") if err != nil { t.Fatal(err) } if network == nil { t.Fatal("no network") } if network.ID != 1 { t.Errorf("unexpected network ID: %v", network.ID) } }) } func TestNetworkClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=mynet" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.NetworkListResponse{ Networks: []schema.Network{}, }) }) ctx := context.Background() network, _, err := env.Client.Network.GetByName(ctx, "mynet") if err != nil { t.Fatal(err) } if network != nil { t.Fatal("unexpected network") } } func TestNetworkCreate(t *testing.T) { var ( ctx = context.Background() _, ipRange, _ = net.ParseCIDR("10.0.1.0/24") ) t.Run("missing required field name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() opts := NetworkCreateOpts{} _, _, err := env.Client.Network.Create(ctx, opts) if err == nil || err.Error() != "missing name" { t.Fatalf("Network.Create should fail with \"missing name\" but failed with %s", err) } }) t.Run("missing required field ip range", func(t *testing.T) { env := newTestEnv() defer env.Teardown() opts := NetworkCreateOpts{ Name: "my-network", } _, _, err := env.Client.Network.Create(ctx, opts) if err == nil || err.Error() != "missing IP range" { t.Fatalf("Network.Create should fail with \"missing IP range\" but failed with %s", err) } }) t.Run("required fields", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "my-network" { t.Errorf("unexpected Name: %v", reqBody.Name) } if reqBody.IPRange != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.NetworkCreateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkCreateOpts{ Name: "my-network", IPRange: ipRange, } _, _, err := env.Client.Network.Create(ctx, opts) if err != nil { t.Fatal(err) } }) } func TestNetworkDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { return }) var ( ctx = context.Background() network = &Network{ID: 1} ) _, err := env.Client.Network.Delete(ctx, network) if err != nil { t.Fatal(err) } } func TestNetworkClientUpdate(t *testing.T) { var ( ctx = context.Background() network = &Network{ID: 1} ) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.NetworkUpdateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkUpdateOpts{ Name: "test", } updatedNetwork, _, err := env.Client.Network.Update(ctx, network, opts) if err != nil { t.Fatal(err) } if updatedNetwork.ID != 1 { t.Errorf("unexpected network ID: %v", updatedNetwork.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.NetworkUpdateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkUpdateOpts{ Labels: map[string]string{"key": "value"}, } updatedNetwork, _, err := env.Client.Network.Update(ctx, network, opts) if err != nil { t.Fatal(err) } if updatedNetwork.ID != 1 { t.Errorf("unexpected network ID: %v", updatedNetwork.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.NetworkUpdateResponse{ Network: schema.Network{ ID: 1, }, }) }) opts := NetworkUpdateOpts{} updatedNetwork, _, err := env.Client.Network.Update(ctx, network, opts) if err != nil { t.Fatal(err) } if updatedNetwork.ID != 1 { t.Errorf("unexpected network ID: %v", updatedNetwork.ID) } }) } func TestNetworkClientChangeIPRange(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/change_ip_range", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkActionChangeIPRangeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.IPRange != "10.0.1.0/24" { t.Errorf("unexpected type: %v", reqBody.IPRange) } json.NewEncoder(w).Encode(schema.NetworkActionChangeIPRangeResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, newIPRange, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkChangeIPRangeOpts{ IPRange: newIPRange, } action, _, err := env.Client.Network.ChangeIPRange(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientAddSubnet(t *testing.T) { t.Run("type server with ip range", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/add_subnet", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkActionAddSubnetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != "server" { t.Errorf("unexpected Type: %v", reqBody.Type) } if reqBody.IPRange != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", reqBody.IPRange) } if reqBody.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", reqBody.NetworkZone) } json.NewEncoder(w).Encode(schema.NetworkActionAddSubnetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, ipRange, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkAddSubnetOpts{ Subnet: NetworkSubnet{ Type: NetworkSubnetTypeServer, IPRange: ipRange, NetworkZone: NetworkZoneEUCentral, }, } action, _, err := env.Client.Network.AddSubnet(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("type server without ip range", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/add_subnet", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.NetworkActionAddSubnetRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type != "server" { t.Errorf("unexpected Type: %v", reqBody.Type) } if reqBody.IPRange != "" { t.Errorf("unexpected IPRange: %v", reqBody.IPRange) } if reqBody.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", reqBody.NetworkZone) } json.NewEncoder(w).Encode(schema.NetworkActionAddSubnetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() opts := NetworkAddSubnetOpts{ Subnet: NetworkSubnet{ Type: NetworkSubnetTypeServer, NetworkZone: NetworkZoneEUCentral, }, } action, _, err := env.Client.Network.AddSubnet(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestNetworkClientDeleteSubnet(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/delete_subnet", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkActionDeleteSubnetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, ipRange, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkDeleteSubnetOpts{ Subnet: NetworkSubnet{ IPRange: ipRange, }, } action, _, err := env.Client.Network.DeleteSubnet(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientAddRoute(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/add_route", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkActionAddRouteResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, destination, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkAddRouteOpts{ Route: NetworkRoute{ Destination: destination, Gateway: net.ParseIP("10.0.1.1"), }, } action, _, err := env.Client.Network.AddRoute(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientDeleteRoute(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/delete_route", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.NetworkActionDeleteRouteResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() _, destination, _ := net.ParseCIDR("10.0.1.0/24") opts := NetworkDeleteRouteOpts{ Route: NetworkRoute{ Destination: destination, Gateway: net.ParseIP("10.0.1.1"), }, } action, _, err := env.Client.Network.DeleteRoute(ctx, &Network{ID: 1}, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestNetworkClientChangeProtection(t *testing.T) { var ( ctx = context.Background() network = &Network{ID: 1} ) t.Run("enable delete protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/networks/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.NetworkActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } json.NewEncoder(w).Encode(schema.NetworkActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := NetworkChangeProtectionOpts{ Delete: Bool(true), } action, _, err := env.Client.Network.ChangeProtection(ctx, network, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } hcloud-go-1.17.0/hcloud/pricing.go000066400000000000000000000036231356226054300167330ustar00rootroot00000000000000package hcloud import ( "context" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Pricing specifies pricing information for various resources. type Pricing struct { Image ImagePricing FloatingIP FloatingIPPricing Traffic TrafficPricing ServerBackup ServerBackupPricing ServerTypes []ServerTypePricing } // Price represents a price. Net amount, gross amount, as well as VAT rate are // specified as strings and it is the user's responsibility to convert them to // appropriate types for calculations. type Price struct { Currency string VATRate string Net string Gross string } // ImagePricing provides pricing information for imaegs. type ImagePricing struct { PerGBMonth Price } // FloatingIPPricing provides pricing information for Floating IPs. type FloatingIPPricing struct { Monthly Price } // TrafficPricing provides pricing information for traffic. type TrafficPricing struct { PerTB Price } // ServerBackupPricing provides pricing information for server backups. type ServerBackupPricing struct { Percentage string } // ServerTypePricing provides pricing information for a server type. type ServerTypePricing struct { ServerType *ServerType Pricings []ServerTypeLocationPricing } // ServerTypeLocationPricing provides pricing information for a server type // at a location. type ServerTypeLocationPricing struct { Location *Location Hourly Price Monthly Price } // PricingClient is a client for the pricing API. type PricingClient struct { client *Client } // Get retrieves pricing information. func (c *PricingClient) Get(ctx context.Context) (Pricing, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", "/pricing", nil) if err != nil { return Pricing{}, nil, err } var body schema.PricingGetResponse resp, err := c.client.Do(req, &body) if err != nil { return Pricing{}, nil, err } return PricingFromSchema(body.Pricing), resp, nil } hcloud-go-1.17.0/hcloud/pricing_test.go000066400000000000000000000012171356226054300177670ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestPricingClientGet(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/pricing", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.PricingGetResponse{ Pricing: schema.Pricing{ Currency: "EUR", }, }) }) ctx := context.Background() pricing, _, err := env.Client.Pricing.Get(ctx) if err != nil { t.Fatal(err) } if pricing.Image.PerGBMonth.Currency != "EUR" { t.Errorf("unexpected currency: %v", pricing.Image.PerGBMonth.Currency) } } hcloud-go-1.17.0/hcloud/schema.go000066400000000000000000000301231356226054300165330ustar00rootroot00000000000000package hcloud import ( "net" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // This file provides converter functions to convert models in the // schema package to models in the hcloud package. // ActionFromSchema converts a schema.Action to an Action. func ActionFromSchema(s schema.Action) *Action { action := &Action{ ID: s.ID, Status: ActionStatus(s.Status), Command: s.Command, Progress: s.Progress, Started: s.Started, Resources: []*ActionResource{}, } if s.Finished != nil { action.Finished = *s.Finished } if s.Error != nil { action.ErrorCode = s.Error.Code action.ErrorMessage = s.Error.Message } for _, r := range s.Resources { action.Resources = append(action.Resources, &ActionResource{ ID: r.ID, Type: ActionResourceType(r.Type), }) } return action } // ActionsFromSchema converts a slice of schema.Action to a slice of Action. func ActionsFromSchema(s []schema.Action) []*Action { var actions []*Action for _, a := range s { actions = append(actions, ActionFromSchema(a)) } return actions } // FloatingIPFromSchema converts a schema.FloatingIP to a FloatingIP. func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP { f := &FloatingIP{ ID: s.ID, Type: FloatingIPType(s.Type), HomeLocation: LocationFromSchema(s.HomeLocation), Created: s.Created, Blocked: s.Blocked, Protection: FloatingIPProtection{ Delete: s.Protection.Delete, }, Name: s.Name, } if s.Description != nil { f.Description = *s.Description } if s.Server != nil { f.Server = &Server{ID: *s.Server} } if f.Type == FloatingIPTypeIPv4 { f.IP = net.ParseIP(s.IP) } else { f.IP, f.Network, _ = net.ParseCIDR(s.IP) } f.DNSPtr = map[string]string{} for _, entry := range s.DNSPtr { f.DNSPtr[entry.IP] = entry.DNSPtr } f.Labels = map[string]string{} for key, value := range s.Labels { f.Labels[key] = value } return f } // ISOFromSchema converts a schema.ISO to an ISO. func ISOFromSchema(s schema.ISO) *ISO { return &ISO{ ID: s.ID, Name: s.Name, Description: s.Description, Type: ISOType(s.Type), Deprecated: s.Deprecated, } } // LocationFromSchema converts a schema.Location to a Location. func LocationFromSchema(s schema.Location) *Location { return &Location{ ID: s.ID, Name: s.Name, Description: s.Description, Country: s.Country, City: s.City, Latitude: s.Latitude, Longitude: s.Longitude, NetworkZone: NetworkZone(s.NetworkZone), } } // DatacenterFromSchema converts a schema.Datacenter to a Datacenter. func DatacenterFromSchema(s schema.Datacenter) *Datacenter { d := &Datacenter{ ID: s.ID, Name: s.Name, Description: s.Description, Location: LocationFromSchema(s.Location), ServerTypes: DatacenterServerTypes{ Available: []*ServerType{}, Supported: []*ServerType{}, }, } for _, t := range s.ServerTypes.Available { d.ServerTypes.Available = append(d.ServerTypes.Available, &ServerType{ID: t}) } for _, t := range s.ServerTypes.Supported { d.ServerTypes.Supported = append(d.ServerTypes.Supported, &ServerType{ID: t}) } return d } // ServerFromSchema converts a schema.Server to a Server. func ServerFromSchema(s schema.Server) *Server { server := &Server{ ID: s.ID, Name: s.Name, Status: ServerStatus(s.Status), Created: s.Created, PublicNet: ServerPublicNetFromSchema(s.PublicNet), ServerType: ServerTypeFromSchema(s.ServerType), IncludedTraffic: s.IncludedTraffic, RescueEnabled: s.RescueEnabled, Datacenter: DatacenterFromSchema(s.Datacenter), Locked: s.Locked, Protection: ServerProtection{ Delete: s.Protection.Delete, Rebuild: s.Protection.Rebuild, }, } if s.Image != nil { server.Image = ImageFromSchema(*s.Image) } if s.BackupWindow != nil { server.BackupWindow = *s.BackupWindow } if s.OutgoingTraffic != nil { server.OutgoingTraffic = *s.OutgoingTraffic } if s.IngoingTraffic != nil { server.IngoingTraffic = *s.IngoingTraffic } if s.ISO != nil { server.ISO = ISOFromSchema(*s.ISO) } server.Labels = map[string]string{} for key, value := range s.Labels { server.Labels[key] = value } for _, id := range s.Volumes { server.Volumes = append(server.Volumes, &Volume{ID: id}) } for _, privNet := range s.PrivateNet { server.PrivateNet = append(server.PrivateNet, ServerPrivateNetFromSchema(privNet)) } return server } // ServerPublicNetFromSchema converts a schema.ServerPublicNet to a ServerPublicNet. func ServerPublicNetFromSchema(s schema.ServerPublicNet) ServerPublicNet { publicNet := ServerPublicNet{ IPv4: ServerPublicNetIPv4FromSchema(s.IPv4), IPv6: ServerPublicNetIPv6FromSchema(s.IPv6), } for _, id := range s.FloatingIPs { publicNet.FloatingIPs = append(publicNet.FloatingIPs, &FloatingIP{ID: id}) } return publicNet } // ServerPublicNetIPv4FromSchema converts a schema.ServerPublicNetIPv4 to // a ServerPublicNetIPv4. func ServerPublicNetIPv4FromSchema(s schema.ServerPublicNetIPv4) ServerPublicNetIPv4 { return ServerPublicNetIPv4{ IP: net.ParseIP(s.IP), Blocked: s.Blocked, DNSPtr: s.DNSPtr, } } // ServerPublicNetIPv6FromSchema converts a schema.ServerPublicNetIPv6 to // a ServerPublicNetIPv6. func ServerPublicNetIPv6FromSchema(s schema.ServerPublicNetIPv6) ServerPublicNetIPv6 { ipv6 := ServerPublicNetIPv6{ Blocked: s.Blocked, DNSPtr: map[string]string{}, } ipv6.IP, ipv6.Network, _ = net.ParseCIDR(s.IP) for _, dnsPtr := range s.DNSPtr { ipv6.DNSPtr[dnsPtr.IP] = dnsPtr.DNSPtr } return ipv6 } // ServerPrivateNetFromSchema converts a schema.ServerPrivateNet to a ServerPrivateNet. func ServerPrivateNetFromSchema(s schema.ServerPrivateNet) ServerPrivateNet { n := ServerPrivateNet{ Network: &Network{ID: s.Network}, IP: net.ParseIP(s.IP), MACAddress: s.MACAddress, } for _, ip := range s.AliasIPs { n.Aliases = append(n.Aliases, net.ParseIP(ip)) } return n } // ServerTypeFromSchema converts a schema.ServerType to a ServerType. func ServerTypeFromSchema(s schema.ServerType) *ServerType { st := &ServerType{ ID: s.ID, Name: s.Name, Description: s.Description, Cores: s.Cores, Memory: s.Memory, Disk: s.Disk, StorageType: StorageType(s.StorageType), CPUType: CPUType(s.CPUType), } for _, price := range s.Prices { st.Pricings = append(st.Pricings, ServerTypeLocationPricing{ Location: &Location{Name: price.Location}, Hourly: Price{ Net: price.PriceHourly.Net, Gross: price.PriceHourly.Gross, }, Monthly: Price{ Net: price.PriceMonthly.Net, Gross: price.PriceMonthly.Gross, }, }) } return st } // SSHKeyFromSchema converts a schema.SSHKey to a SSHKey. func SSHKeyFromSchema(s schema.SSHKey) *SSHKey { sshKey := &SSHKey{ ID: s.ID, Name: s.Name, Fingerprint: s.Fingerprint, PublicKey: s.PublicKey, Created: s.Created, } sshKey.Labels = map[string]string{} for key, value := range s.Labels { sshKey.Labels[key] = value } return sshKey } // ImageFromSchema converts a schema.Image to an Image. func ImageFromSchema(s schema.Image) *Image { i := &Image{ ID: s.ID, Type: ImageType(s.Type), Status: ImageStatus(s.Status), Description: s.Description, DiskSize: s.DiskSize, Created: s.Created, RapidDeploy: s.RapidDeploy, OSFlavor: s.OSFlavor, Protection: ImageProtection{ Delete: s.Protection.Delete, }, Deprecated: s.Deprecated, } if s.Name != nil { i.Name = *s.Name } if s.ImageSize != nil { i.ImageSize = *s.ImageSize } if s.OSVersion != nil { i.OSVersion = *s.OSVersion } if s.CreatedFrom != nil { i.CreatedFrom = &Server{ ID: s.CreatedFrom.ID, Name: s.CreatedFrom.Name, } } if s.BoundTo != nil { i.BoundTo = &Server{ ID: *s.BoundTo, } } i.Labels = map[string]string{} for key, value := range s.Labels { i.Labels[key] = value } return i } // VolumeFromSchema converts a schema.Volume to a Volume. func VolumeFromSchema(s schema.Volume) *Volume { v := &Volume{ ID: s.ID, Name: s.Name, Location: LocationFromSchema(s.Location), Size: s.Size, LinuxDevice: s.LinuxDevice, Protection: VolumeProtection{ Delete: s.Protection.Delete, }, Created: s.Created, } if s.Server != nil { v.Server = &Server{ID: *s.Server} } v.Labels = map[string]string{} for key, value := range s.Labels { v.Labels[key] = value } return v } // NetworkFromSchema converts a schema.Network to a Network. func NetworkFromSchema(s schema.Network) *Network { n := &Network{ ID: s.ID, Name: s.Name, Created: s.Created, Protection: NetworkProtection{ Delete: s.Protection.Delete, }, Labels: map[string]string{}, } _, n.IPRange, _ = net.ParseCIDR(s.IPRange) for _, subnet := range s.Subnets { n.Subnets = append(n.Subnets, NetworkSubnetFromSchema(subnet)) } for _, route := range s.Routes { n.Routes = append(n.Routes, NetworkRouteFromSchema(route)) } for _, serverID := range s.Servers { n.Servers = append(n.Servers, &Server{ID: serverID}) } for key, value := range s.Labels { n.Labels[key] = value } return n } // NetworkSubnetFromSchema converts a schema.NetworkSubnet to a NetworkSubnet. func NetworkSubnetFromSchema(s schema.NetworkSubnet) NetworkSubnet { sn := NetworkSubnet{ Type: NetworkSubnetType(s.Type), NetworkZone: NetworkZone(s.NetworkZone), Gateway: net.ParseIP(s.Gateway), } _, sn.IPRange, _ = net.ParseCIDR(s.IPRange) return sn } // NetworkRouteFromSchema converts a schema.NetworkRoute to a NetworkRoute. func NetworkRouteFromSchema(s schema.NetworkRoute) NetworkRoute { r := NetworkRoute{ Gateway: net.ParseIP(s.Gateway), } _, r.Destination, _ = net.ParseCIDR(s.Destination) return r } // PaginationFromSchema converts a schema.MetaPagination to a Pagination. func PaginationFromSchema(s schema.MetaPagination) Pagination { return Pagination{ Page: s.Page, PerPage: s.PerPage, PreviousPage: s.PreviousPage, NextPage: s.NextPage, LastPage: s.LastPage, TotalEntries: s.TotalEntries, } } // ErrorFromSchema converts a schema.Error to an Error. func ErrorFromSchema(s schema.Error) Error { e := Error{ Code: ErrorCode(s.Code), Message: s.Message, } switch d := s.Details.(type) { case schema.ErrorDetailsInvalidInput: details := ErrorDetailsInvalidInput{ Fields: []ErrorDetailsInvalidInputField{}, } for _, field := range d.Fields { details.Fields = append(details.Fields, ErrorDetailsInvalidInputField{ Name: field.Name, Messages: field.Messages, }) } e.Details = details } return e } // PricingFromSchema converts a schema.Pricing to a Pricing. func PricingFromSchema(s schema.Pricing) Pricing { p := Pricing{ Image: ImagePricing{ PerGBMonth: Price{ Currency: s.Currency, VATRate: s.VATRate, Net: s.Image.PricePerGBMonth.Net, Gross: s.Image.PricePerGBMonth.Gross, }, }, FloatingIP: FloatingIPPricing{ Monthly: Price{ Currency: s.Currency, VATRate: s.VATRate, Net: s.FloatingIP.PriceMonthly.Net, Gross: s.FloatingIP.PriceMonthly.Gross, }, }, Traffic: TrafficPricing{ PerTB: Price{ Currency: s.Currency, VATRate: s.VATRate, Net: s.Traffic.PricePerTB.Net, Gross: s.Traffic.PricePerTB.Gross, }, }, ServerBackup: ServerBackupPricing{ Percentage: s.ServerBackup.Percentage, }, } for _, serverType := range s.ServerTypes { var pricings []ServerTypeLocationPricing for _, price := range serverType.Prices { pricings = append(pricings, ServerTypeLocationPricing{ Location: &Location{Name: price.Location}, Hourly: Price{ Currency: s.Currency, VATRate: s.VATRate, Net: price.PriceHourly.Net, Gross: price.PriceHourly.Gross, }, Monthly: Price{ Currency: s.Currency, VATRate: s.VATRate, Net: price.PriceMonthly.Net, Gross: price.PriceMonthly.Gross, }, }) } p.ServerTypes = append(p.ServerTypes, ServerTypePricing{ ServerType: &ServerType{ ID: serverType.ID, Name: serverType.Name, }, Pricings: pricings, }) } return p } hcloud-go-1.17.0/hcloud/schema/000077500000000000000000000000001356226054300162055ustar00rootroot00000000000000hcloud-go-1.17.0/hcloud/schema/action.go000066400000000000000000000022141356226054300200100ustar00rootroot00000000000000package schema import "time" // Action defines the schema of an action. type Action struct { ID int `json:"id"` Status string `json:"status"` Command string `json:"command"` Progress int `json:"progress"` Started time.Time `json:"started"` Finished *time.Time `json:"finished"` Error *ActionError `json:"error"` Resources []ActionResourceReference `json:"resources"` } // ActionResourceReference defines the schema of an action resource reference. type ActionResourceReference struct { ID int `json:"id"` Type string `json:"type"` } // ActionError defines the schema of an error embedded // in an action. type ActionError struct { Code string `json:"code"` Message string `json:"message"` } // ActionGetResponse is the schema of the response when // retrieving a single action. type ActionGetResponse struct { Action Action `json:"action"` } // ActionListResponse defines the schema of the response when listing actions. type ActionListResponse struct { Actions []Action `json:"actions"` } hcloud-go-1.17.0/hcloud/schema/datacenter.go000066400000000000000000000013201356226054300206420ustar00rootroot00000000000000package schema // Datacenter defines the schema of a datacenter. type Datacenter struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` Location Location `json:"location"` ServerTypes struct { Supported []int `json:"supported"` Available []int `json:"available"` } `json:"server_types"` } // DatacenterGetResponse defines the schema of the response when retrieving a single datacenter. type DatacenterGetResponse struct { Datacenter Datacenter `json:"datacenter"` } // DatacenterListResponse defines the schema of the response when listing datacenters. type DatacenterListResponse struct { Datacenters []Datacenter `json:"datacenters"` } hcloud-go-1.17.0/hcloud/schema/error.go000066400000000000000000000020651356226054300176700ustar00rootroot00000000000000package schema import "encoding/json" // Error represents the schema of an error response. type Error struct { Code string `json:"code"` Message string `json:"message"` DetailsRaw json.RawMessage `json:"details"` Details interface{} } // UnmarshalJSON overrides default json unmarshalling. func (e *Error) UnmarshalJSON(data []byte) (err error) { type Alias Error alias := (*Alias)(e) if err = json.Unmarshal(data, alias); err != nil { return } switch e.Code { case "invalid_input": details := ErrorDetailsInvalidInput{} if err = json.Unmarshal(e.DetailsRaw, &details); err != nil { return } alias.Details = details } return } // ErrorResponse defines the schema of a response containing an error. type ErrorResponse struct { Error Error `json:"error"` } // ErrorDetailsInvalidInput defines the schema of the Details field // of an error with code 'invalid_input'. type ErrorDetailsInvalidInput struct { Fields []struct { Name string `json:"name"` Messages []string `json:"messages"` } `json:"fields"` } hcloud-go-1.17.0/hcloud/schema/error_test.go000066400000000000000000000025171356226054300207310ustar00rootroot00000000000000package schema import ( "encoding/json" "testing" ) func TestError(t *testing.T) { t.Run("UnmarshalJSON", func(t *testing.T) { data := []byte(`{ "code": "invalid_input", "message": "invalid input", "details": { "fields": [ { "name": "broken_field", "messages": ["is required"] } ] } }`) e := &Error{} err := json.Unmarshal(data, e) if err != nil { t.Fatalf("unexpected error: %v", err) } if e.Code != "invalid_input" { t.Errorf("unexpected Code: %v", e.Code) } if e.Message != "invalid input" { t.Errorf("unexpected Message: %v", e.Message) } if e.Details == nil { t.Fatalf("unexpected Details: %v", e.Details) } d, ok := e.Details.(ErrorDetailsInvalidInput) if !ok { t.Fatalf("unexpected Details type (should be ErrorDetailsInvalidInput): %v", e.Details) } if len(d.Fields) != 1 { t.Fatalf("unexpected Details.Fields length (should be 1): %v", d.Fields) } if d.Fields[0].Name != "broken_field" { t.Errorf("unexpected Details.Fields[0].Name: %v", d.Fields[0].Name) } if len(d.Fields[0].Messages) != 1 { t.Fatalf("unexpected Details.Fields[0].Messages length (should be 1): %v", d.Fields[0].Messages) } if d.Fields[0].Messages[0] != "is required" { t.Errorf("unexpected Details.Fields[0].Messages[0]: %v", d.Fields[0].Messages[0]) } }) } hcloud-go-1.17.0/hcloud/schema/floating_ip.go000066400000000000000000000101711356226054300210270ustar00rootroot00000000000000package schema import "time" // FloatingIP defines the schema of a Floating IP. type FloatingIP struct { ID int `json:"id"` Description *string `json:"description"` Created time.Time `json:"created"` IP string `json:"ip"` Type string `json:"type"` Server *int `json:"server"` DNSPtr []FloatingIPDNSPtr `json:"dns_ptr"` HomeLocation Location `json:"home_location"` Blocked bool `json:"blocked"` Protection FloatingIPProtection `json:"protection"` Labels map[string]string `json:"labels"` Name string `json:"name"` } // FloatingIPProtection represents the protection level of a Floating IP. type FloatingIPProtection struct { Delete bool `json:"delete"` } // FloatingIPDNSPtr contains reverse DNS information for a // IPv4 or IPv6 Floating IP. type FloatingIPDNSPtr struct { IP string `json:"ip"` DNSPtr string `json:"dns_ptr"` } // FloatingIPGetResponse defines the schema of the response when // retrieving a single Floating IP. type FloatingIPGetResponse struct { FloatingIP FloatingIP `json:"floating_ip"` } // FloatingIPUpdateRequest defines the schema of the request to update a Floating IP. type FloatingIPUpdateRequest struct { Description string `json:"description,omitempty"` Labels *map[string]string `json:"labels,omitempty"` Name string `json:"name,omitempty"` } // FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP. type FloatingIPUpdateResponse struct { FloatingIP FloatingIP `json:"floating_ip"` } // FloatingIPListResponse defines the schema of the response when // listing Floating IPs. type FloatingIPListResponse struct { FloatingIPs []FloatingIP `json:"floating_ips"` } // FloatingIPCreateRequest defines the schema of the request to // create a Floating IP. type FloatingIPCreateRequest struct { Type string `json:"type"` HomeLocation *string `json:"home_location,omitempty"` Server *int `json:"server,omitempty"` Description *string `json:"description,omitempty"` Labels *map[string]string `json:"labels,omitempty"` Name *string `json:"name,omitempty"` } // FloatingIPCreateResponse defines the schema of the response // when creating a Floating IP. type FloatingIPCreateResponse struct { FloatingIP FloatingIP `json:"floating_ip"` Action *Action `json:"action"` } // FloatingIPActionAssignRequest defines the schema of the request to // create an assign Floating IP action. type FloatingIPActionAssignRequest struct { Server int `json:"server"` } // FloatingIPActionAssignResponse defines the schema of the response when // creating an assign action. type FloatingIPActionAssignResponse struct { Action Action `json:"action"` } // FloatingIPActionUnassignRequest defines the schema of the request to // create an unassign Floating IP action. type FloatingIPActionUnassignRequest struct{} // FloatingIPActionUnassignResponse defines the schema of the response when // creating an unassign action. type FloatingIPActionUnassignResponse struct { Action Action `json:"action"` } // FloatingIPActionChangeDNSPtrRequest defines the schema for the request to // change a Floating IP's reverse DNS pointer. type FloatingIPActionChangeDNSPtrRequest struct { IP string `json:"ip"` DNSPtr *string `json:"dns_ptr"` } // FloatingIPActionChangeDNSPtrResponse defines the schema of the response when // creating a change_dns_ptr Floating IP action. type FloatingIPActionChangeDNSPtrResponse struct { Action Action `json:"action"` } // FloatingIPActionChangeProtectionRequest defines the schema of the request to change the resource protection of a Floating IP. type FloatingIPActionChangeProtectionRequest struct { Delete *bool `json:"delete,omitempty"` } // FloatingIPActionChangeProtectionResponse defines the schema of the response when changing the resource protection of a Floating IP. type FloatingIPActionChangeProtectionResponse struct { Action Action `json:"action"` } hcloud-go-1.17.0/hcloud/schema/floating_ip_test.go000066400000000000000000000022501356226054300220650ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestFloatingIPCreateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in FloatingIPCreateRequest out []byte }{ { name: "no labels", in: FloatingIPCreateRequest{Type: "ipv4"}, out: []byte(`{"type":"ipv4"}`), }, { name: "one label", in: FloatingIPCreateRequest{Type: "ipv4", Labels: &oneLabel}, out: []byte(`{"type":"ipv4","labels":{"foo":"bar"}}`), }, { name: "nil labels", in: FloatingIPCreateRequest{Type: "ipv4", Labels: &nilLabels}, out: []byte(`{"type":"ipv4","labels":null}`), }, { name: "empty labels", in: FloatingIPCreateRequest{Type: "ipv4", Labels: &emptyLabels}, out: []byte(`{"type":"ipv4","labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } hcloud-go-1.17.0/hcloud/schema/image.go000066400000000000000000000044231356226054300176210ustar00rootroot00000000000000package schema import "time" // Image defines the schema of an image. type Image struct { ID int `json:"id"` Status string `json:"status"` Type string `json:"type"` Name *string `json:"name"` Description string `json:"description"` ImageSize *float32 `json:"image_size"` DiskSize float32 `json:"disk_size"` Created time.Time `json:"created"` CreatedFrom *ImageCreatedFrom `json:"created_from"` BoundTo *int `json:"bound_to"` OSFlavor string `json:"os_flavor"` OSVersion *string `json:"os_version"` RapidDeploy bool `json:"rapid_deploy"` Protection ImageProtection `json:"protection"` Deprecated time.Time `json:"deprecated"` Labels map[string]string `json:"labels"` } // ImageProtection represents the protection level of a image. type ImageProtection struct { Delete bool `json:"delete"` } // ImageCreatedFrom defines the schema of the images created from reference. type ImageCreatedFrom struct { ID int `json:"id"` Name string `json:"name"` } // ImageGetResponse defines the schema of the response when // retrieving a single image. type ImageGetResponse struct { Image Image `json:"image"` } // ImageListResponse defines the schema of the response when // listing images. type ImageListResponse struct { Images []Image `json:"images"` } // ImageUpdateRequest defines the schema of the request to update an image. type ImageUpdateRequest struct { Description *string `json:"description,omitempty"` Type *string `json:"type,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // ImageUpdateResponse defines the schema of the response when updating an image. type ImageUpdateResponse struct { Image Image `json:"image"` } // ImageActionChangeProtectionRequest defines the schema of the request to change the resource protection of an image. type ImageActionChangeProtectionRequest struct { Delete *bool `json:"delete,omitempty"` } // ImageActionChangeProtectionResponse defines the schema of the response when changing the resource protection of an image. type ImageActionChangeProtectionResponse struct { Action Action `json:"action"` } hcloud-go-1.17.0/hcloud/schema/image_test.go000066400000000000000000000020351356226054300206550ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestImageUpdateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in ImageUpdateRequest out []byte }{ { name: "no labels", in: ImageUpdateRequest{}, out: []byte(`{}`), }, { name: "one label", in: ImageUpdateRequest{Labels: &oneLabel}, out: []byte(`{"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: ImageUpdateRequest{Labels: &nilLabels}, out: []byte(`{"labels":null}`), }, { name: "empty labels", in: ImageUpdateRequest{Labels: &emptyLabels}, out: []byte(`{"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } hcloud-go-1.17.0/hcloud/schema/iso.go000066400000000000000000000010571356226054300173310ustar00rootroot00000000000000package schema import "time" // ISO defines the schema of an ISO image. type ISO struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` Deprecated time.Time `json:"deprecated"` } // ISOGetResponse defines the schema of the response when retrieving a single ISO. type ISOGetResponse struct { ISO ISO `json:"iso"` } // ISOListResponse defines the schema of the response when listing ISOs. type ISOListResponse struct { ISOs []ISO `json:"isos"` } hcloud-go-1.17.0/hcloud/schema/location.go000066400000000000000000000013211356226054300203410ustar00rootroot00000000000000package schema // Location defines the schema of a location. type Location struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` Country string `json:"country"` City string `json:"city"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` NetworkZone string `json:"network_zone"` } // LocationGetResponse defines the schema of the response when retrieving a single location. type LocationGetResponse struct { Location Location `json:"location"` } // LocationListResponse defines the schema of the response when listing locations. type LocationListResponse struct { Locations []Location `json:"locations"` } hcloud-go-1.17.0/hcloud/schema/meta.go000066400000000000000000000011631356226054300174630ustar00rootroot00000000000000package schema // Meta defines the schema of meta information which may be included // in responses. type Meta struct { Pagination *MetaPagination `json:"pagination"` } // MetaPagination defines the schema of pagination information. type MetaPagination struct { Page int `json:"page"` PerPage int `json:"per_page"` PreviousPage int `json:"previous_page"` NextPage int `json:"next_page"` LastPage int `json:"last_page"` TotalEntries int `json:"total_entries"` } // MetaResponse defines the schema of a response containing // meta information. type MetaResponse struct { Meta Meta `json:"meta"` } hcloud-go-1.17.0/hcloud/schema/network.go000066400000000000000000000113151356226054300202260ustar00rootroot00000000000000package schema import "time" // Network defines the schema of a network. type Network struct { ID int `json:"id"` Name string `json:"name"` Created time.Time `json:"created"` IPRange string `json:"ip_range"` Subnets []NetworkSubnet `json:"subnets"` Routes []NetworkRoute `json:"routes"` Servers []int `json:"servers"` Protection NetworkProtection `json:"protection"` Labels map[string]string `json:"labels"` } // NetworkSubnet represents a subnet of a network. type NetworkSubnet struct { Type string `json:"type"` IPRange string `json:"ip_range"` NetworkZone string `json:"network_zone"` Gateway string `json:"gateway"` } // NetworkRoute represents a route of a network. type NetworkRoute struct { Destination string `json:"destination"` Gateway string `json:"gateway"` } // NetworkProtection represents the protection level of a network. type NetworkProtection struct { Delete bool `json:"delete"` } // NetworkUpdateRequest defines the schema of the request to update a network. type NetworkUpdateRequest struct { Name string `json:"name,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // NetworkUpdateResponse defines the schema of the response when updating a network. type NetworkUpdateResponse struct { Network Network `json:"network"` } // NetworkListResponse defines the schema of the response when // listing networks. type NetworkListResponse struct { Networks []Network `json:"networks"` } // NetworkGetResponse defines the schema of the response when // retrieving a single network. type NetworkGetResponse struct { Network Network `json:"network"` } // NetworkCreateRequest defines the schema of the request to create a network. type NetworkCreateRequest struct { Name string `json:"name"` IPRange string `json:"ip_range"` Subnets []NetworkSubnet `json:"subnets,omitempty"` Routes []NetworkRoute `json:"routes,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // NetworkCreateResponse defines the schema of the response when // creating a network. type NetworkCreateResponse struct { Network Network `json:"network"` } // NetworkActionChangeIPRangeRequest defines the schema of the request to // change the IP range of a network. type NetworkActionChangeIPRangeRequest struct { IPRange string `json:"ip_range"` } // NetworkActionChangeIPRangeResponse defines the schema of the response when // changing the IP range of a network. type NetworkActionChangeIPRangeResponse struct { Action Action `json:"action"` } // NetworkActionAddSubnetRequest defines the schema of the request to // add a subnet to a network. type NetworkActionAddSubnetRequest struct { Type string `json:"type"` IPRange string `json:"ip_range,omitempty"` NetworkZone string `json:"network_zone"` Gateway string `json:"gateway"` } // NetworkActionAddSubnetResponse defines the schema of the response when // adding a subnet to a network. type NetworkActionAddSubnetResponse struct { Action Action `json:"action"` } // NetworkActionDeleteSubnetRequest defines the schema of the request to // delete a subnet from a network. type NetworkActionDeleteSubnetRequest struct { IPRange string `json:"ip_range"` } // NetworkActionDeleteSubnetResponse defines the schema of the response when // deleting a subnet from a network. type NetworkActionDeleteSubnetResponse struct { Action Action `json:"action"` } // NetworkActionAddRouteRequest defines the schema of the request to // add a route to a network. type NetworkActionAddRouteRequest struct { Destination string `json:"destination"` Gateway string `json:"gateway"` } // NetworkActionAddRouteResponse defines the schema of the response when // adding a route to a network. type NetworkActionAddRouteResponse struct { Action Action `json:"action"` } // NetworkActionDeleteRouteRequest defines the schema of the request to // delete a route from a network. type NetworkActionDeleteRouteRequest struct { Destination string `json:"destination"` Gateway string `json:"gateway"` } // NetworkActionDeleteRouteResponse defines the schema of the response when // deleting a route from a network. type NetworkActionDeleteRouteResponse struct { Action Action `json:"action"` } // NetworkActionChangeProtectionRequest defines the schema of the request to // change the resource protection of a network. type NetworkActionChangeProtectionRequest struct { Delete *bool `json:"delete,omitempty"` } // NetworkActionChangeProtectionResponse defines the schema of the response when // changing the resource protection of a network. type NetworkActionChangeProtectionResponse struct { Action Action `json:"action"` } hcloud-go-1.17.0/hcloud/schema/network_test.go000066400000000000000000000020511356226054300212620ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestNetworkUpdateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in NetworkUpdateRequest out []byte }{ { name: "no labels", in: NetworkUpdateRequest{}, out: []byte(`{}`), }, { name: "one label", in: NetworkUpdateRequest{Labels: &oneLabel}, out: []byte(`{"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: NetworkUpdateRequest{Labels: &nilLabels}, out: []byte(`{"labels":null}`), }, { name: "empty labels", in: NetworkUpdateRequest{Labels: &emptyLabels}, out: []byte(`{"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } hcloud-go-1.17.0/hcloud/schema/pricing.go000066400000000000000000000036121356226054300201710ustar00rootroot00000000000000package schema // Pricing defines the schema for pricing information. type Pricing struct { Currency string `json:"currency"` VATRate string `json:"vat_rate"` Image PricingImage `json:"image"` FloatingIP PricingFloatingIP `json:"floating_ip"` Traffic PricingTraffic `json:"traffic"` ServerBackup PricingServerBackup `json:"server_backup"` ServerTypes []PricingServerType `json:"server_types"` } // Price defines the schema of a single price with net and gross amount. type Price struct { Net string `json:"net"` Gross string `json:"gross"` } // PricingImage defines the schema of pricing information for an image. type PricingImage struct { PricePerGBMonth Price `json:"price_per_gb_month"` } // PricingFloatingIP defines the schema of pricing information for a Floating IP. type PricingFloatingIP struct { PriceMonthly Price `json:"price_monthly"` } // PricingTraffic defines the schema of pricing information for traffic. type PricingTraffic struct { PricePerTB Price `json:"price_per_tb"` } // PricingServerBackup defines the schema of pricing information for server backups. type PricingServerBackup struct { Percentage string `json:"percentage"` } // PricingServerType defines the schema of pricing information for a server type. type PricingServerType struct { ID int `json:"id"` Name string `json:"name"` Prices []PricingServerTypePrice `json:"prices"` } // PricingServerTypePrice defines the schema of pricing information for a server // type at a location. type PricingServerTypePrice struct { Location string `json:"location"` PriceHourly Price `json:"price_hourly"` PriceMonthly Price `json:"price_monthly"` } // PricingGetResponse defines the schema of the response when retrieving pricing information. type PricingGetResponse struct { Pricing Pricing `json:"pricing"` } hcloud-go-1.17.0/hcloud/schema/server.go000066400000000000000000000303361356226054300200470ustar00rootroot00000000000000package schema import "time" // Server defines the schema of a server. type Server struct { ID int `json:"id"` Name string `json:"name"` Status string `json:"status"` Created time.Time `json:"created"` PublicNet ServerPublicNet `json:"public_net"` PrivateNet []ServerPrivateNet `json:"private_net"` ServerType ServerType `json:"server_type"` IncludedTraffic uint64 `json:"included_traffic"` OutgoingTraffic *uint64 `json:"outgoing_traffic"` IngoingTraffic *uint64 `json:"ingoing_traffic"` BackupWindow *string `json:"backup_window"` RescueEnabled bool `json:"rescue_enabled"` ISO *ISO `json:"iso"` Locked bool `json:"locked"` Datacenter Datacenter `json:"datacenter"` Image *Image `json:"image"` Protection ServerProtection `json:"protection"` Labels map[string]string `json:"labels"` Volumes []int `json:"volumes"` } // ServerProtection defines the schema of a server's resource protection. type ServerProtection struct { Delete bool `json:"delete"` Rebuild bool `json:"rebuild"` } // ServerPublicNet defines the schema of a server's // public network information. type ServerPublicNet struct { IPv4 ServerPublicNetIPv4 `json:"ipv4"` IPv6 ServerPublicNetIPv6 `json:"ipv6"` FloatingIPs []int `json:"floating_ips"` } // ServerPublicNetIPv4 defines the schema of a server's public // network information for an IPv4. type ServerPublicNetIPv4 struct { IP string `json:"ip"` Blocked bool `json:"blocked"` DNSPtr string `json:"dns_ptr"` } // ServerPublicNetIPv6 defines the schema of a server's public // network information for an IPv6. type ServerPublicNetIPv6 struct { IP string `json:"ip"` Blocked bool `json:"blocked"` DNSPtr []ServerPublicNetIPv6DNSPtr `json:"dns_ptr"` } // ServerPublicNetIPv6DNSPtr defines the schema of a server's // public network information for an IPv6 reverse DNS. type ServerPublicNetIPv6DNSPtr struct { IP string `json:"ip"` DNSPtr string `json:"dns_ptr"` } // ServerPrivateNet defines the schema of a server's private network information. type ServerPrivateNet struct { Network int `json:"network"` IP string `json:"ip"` AliasIPs []string `json:"alias_ips"` MACAddress string `json:"mac_address"` } // ServerGetResponse defines the schema of the response when // retrieving a single server. type ServerGetResponse struct { Server Server `json:"server"` } // ServerListResponse defines the schema of the response when // listing servers. type ServerListResponse struct { Servers []Server `json:"servers"` } // ServerCreateRequest defines the schema for the request to // create a server. type ServerCreateRequest struct { Name string `json:"name"` ServerType interface{} `json:"server_type"` // int or string Image interface{} `json:"image"` // int or string SSHKeys []int `json:"ssh_keys,omitempty"` Location string `json:"location,omitempty"` Datacenter string `json:"datacenter,omitempty"` UserData string `json:"user_data,omitempty"` StartAfterCreate *bool `json:"start_after_create,omitempty"` Labels *map[string]string `json:"labels,omitempty"` Automount *bool `json:"automount,omitempty"` Volumes []int `json:"volumes,omitempty"` Networks []int `json:"networks,omitempty"` } // ServerCreateResponse defines the schema of the response when // creating a server. type ServerCreateResponse struct { Server Server `json:"server"` Action Action `json:"action"` RootPassword *string `json:"root_password"` NextActions []Action `json:"next_actions"` } // ServerUpdateRequest defines the schema of the request to update a server. type ServerUpdateRequest struct { Name string `json:"name,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // ServerUpdateResponse defines the schema of the response when updating a server. type ServerUpdateResponse struct { Server Server `json:"server"` } // ServerActionPoweronRequest defines the schema for the request to // create a poweron server action. type ServerActionPoweronRequest struct{} // ServerActionPoweronResponse defines the schema of the response when // creating a poweron server action. type ServerActionPoweronResponse struct { Action Action `json:"action"` } // ServerActionPoweroffRequest defines the schema for the request to // create a poweroff server action. type ServerActionPoweroffRequest struct{} // ServerActionPoweroffResponse defines the schema of the response when // creating a poweroff server action. type ServerActionPoweroffResponse struct { Action Action `json:"action"` } // ServerActionRebootRequest defines the schema for the request to // create a reboot server action. type ServerActionRebootRequest struct{} // ServerActionRebootResponse defines the schema of the response when // creating a reboot server action. type ServerActionRebootResponse struct { Action Action `json:"action"` } // ServerActionResetRequest defines the schema for the request to // create a reset server action. type ServerActionResetRequest struct{} // ServerActionResetResponse defines the schema of the response when // creating a reset server action. type ServerActionResetResponse struct { Action Action `json:"action"` } // ServerActionShutdownRequest defines the schema for the request to // create a shutdown server action. type ServerActionShutdownRequest struct{} // ServerActionShutdownResponse defines the schema of the response when // creating a shutdown server action. type ServerActionShutdownResponse struct { Action Action `json:"action"` } // ServerActionResetPasswordRequest defines the schema for the request to // create a reset_password server action. type ServerActionResetPasswordRequest struct{} // ServerActionResetPasswordResponse defines the schema of the response when // creating a reset_password server action. type ServerActionResetPasswordResponse struct { Action Action `json:"action"` RootPassword string `json:"root_password"` } // ServerActionCreateImageRequest defines the schema for the request to // create a create_image server action. type ServerActionCreateImageRequest struct { Type *string `json:"type"` Description *string `json:"description"` Labels *map[string]string `json:"labels,omitempty"` } // ServerActionCreateImageResponse defines the schema of the response when // creating a create_image server action. type ServerActionCreateImageResponse struct { Action Action `json:"action"` Image Image `json:"image"` } // ServerActionEnableRescueRequest defines the schema for the request to // create a enable_rescue server action. type ServerActionEnableRescueRequest struct { Type *string `json:"type,omitempty"` SSHKeys []int `json:"ssh_keys,omitempty"` } // ServerActionEnableRescueResponse defines the schema of the response when // creating a enable_rescue server action. type ServerActionEnableRescueResponse struct { Action Action `json:"action"` RootPassword string `json:"root_password"` } // ServerActionDisableRescueRequest defines the schema for the request to // create a disable_rescue server action. type ServerActionDisableRescueRequest struct{} // ServerActionDisableRescueResponse defines the schema of the response when // creating a disable_rescue server action. type ServerActionDisableRescueResponse struct { Action Action `json:"action"` } // ServerActionRebuildRequest defines the schema for the request to // rebuild a server. type ServerActionRebuildRequest struct { Image interface{} `json:"image"` // int or string } // ServerActionRebuildResponse defines the schema of the response when // creating a rebuild server action. type ServerActionRebuildResponse struct { Action Action `json:"action"` } // ServerActionAttachISORequest defines the schema for the request to // attach an ISO to a server. type ServerActionAttachISORequest struct { ISO interface{} `json:"iso"` // int or string } // ServerActionAttachISOResponse defines the schema of the response when // creating a attach_iso server action. type ServerActionAttachISOResponse struct { Action Action `json:"action"` } // ServerActionDetachISORequest defines the schema for the request to // detach an ISO from a server. type ServerActionDetachISORequest struct{} // ServerActionDetachISOResponse defines the schema of the response when // creating a detach_iso server action. type ServerActionDetachISOResponse struct { Action Action `json:"action"` } // ServerActionEnableBackupRequest defines the schema for the request to // enable backup for a server. type ServerActionEnableBackupRequest struct { BackupWindow *string `json:"backup_window,omitempty"` } // ServerActionEnableBackupResponse defines the schema of the response when // creating a enable_backup server action. type ServerActionEnableBackupResponse struct { Action Action `json:"action"` } // ServerActionDisableBackupRequest defines the schema for the request to // disable backup for a server. type ServerActionDisableBackupRequest struct{} // ServerActionDisableBackupResponse defines the schema of the response when // creating a disable_backup server action. type ServerActionDisableBackupResponse struct { Action Action `json:"action"` } // ServerActionChangeTypeRequest defines the schema for the request to // change a server's type. type ServerActionChangeTypeRequest struct { ServerType interface{} `json:"server_type"` // int or string UpgradeDisk bool `json:"upgrade_disk"` } // ServerActionChangeTypeResponse defines the schema of the response when // creating a change_type server action. type ServerActionChangeTypeResponse struct { Action Action `json:"action"` } // ServerActionChangeDNSPtrRequest defines the schema for the request to // change a server's reverse DNS pointer. type ServerActionChangeDNSPtrRequest struct { IP string `json:"ip"` DNSPtr *string `json:"dns_ptr"` } // ServerActionChangeDNSPtrResponse defines the schema of the response when // creating a change_dns_ptr server action. type ServerActionChangeDNSPtrResponse struct { Action Action `json:"action"` } // ServerActionChangeProtectionRequest defines the schema of the request to // change the resource protection of a server. type ServerActionChangeProtectionRequest struct { Rebuild *bool `json:"rebuild,omitempty"` Delete *bool `json:"delete,omitempty"` } // ServerActionChangeProtectionResponse defines the schema of the response when // changing the resource protection of a server. type ServerActionChangeProtectionResponse struct { Action Action `json:"action"` } // ServerActionAttachToNetworkRequest defines the schema for the request to // attach a network to a server. type ServerActionAttachToNetworkRequest struct { Network int `json:"network"` IP *string `json:"ip,omitempty"` AliasIPs []*string `json:"alias_ips,omitempty"` } // ServerActionAttachToNetworkResponse defines the schema of the response when // creating an attach_to_network server action. type ServerActionAttachToNetworkResponse struct { Action Action `json:"action"` } // ServerActionDetachFromNetworkRequest defines the schema for the request to // detach a network from a server. type ServerActionDetachFromNetworkRequest struct { Network int `json:"network"` } // ServerActionDetachFromNetworkResponse defines the schema of the response when // creating a detach_from_network server action. type ServerActionDetachFromNetworkResponse struct { Action Action `json:"action"` } // ServerActionChangeAliasIPsRequest defines the schema for the request to // change a server's alias IPs in a network. type ServerActionChangeAliasIPsRequest struct { Network int `json:"network"` AliasIPs []string `json:"alias_ips"` } // ServerActionChangeAliasIPsResponse defines the schema of the response when // creating an change_alias_ips server action. type ServerActionChangeAliasIPsResponse struct { Action Action `json:"action"` } hcloud-go-1.17.0/hcloud/schema/server_test.go000066400000000000000000000023401356226054300211000ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestServerActionCreateImageRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in ServerActionCreateImageRequest out []byte }{ { name: "no labels", in: ServerActionCreateImageRequest{}, out: []byte(`{"type":null,"description":null}`), }, { name: "one label", in: ServerActionCreateImageRequest{Labels: &oneLabel}, out: []byte(`{"type":null,"description":null,"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: ServerActionCreateImageRequest{Labels: &nilLabels}, out: []byte(`{"type":null,"description":null,"labels":null}`), }, { name: "empty labels", in: ServerActionCreateImageRequest{Labels: &emptyLabels}, out: []byte(`{"type":null,"description":null,"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } hcloud-go-1.17.0/hcloud/schema/server_type.go000066400000000000000000000016631356226054300211110ustar00rootroot00000000000000package schema // ServerType defines the schema of a server type. type ServerType struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` Cores int `json:"cores"` Memory float32 `json:"memory"` Disk int `json:"disk"` StorageType string `json:"storage_type"` CPUType string `json:"cpu_type"` Prices []PricingServerTypePrice `json:"prices"` } // ServerTypeListResponse defines the schema of the response when // listing server types. type ServerTypeListResponse struct { ServerTypes []ServerType `json:"server_types"` } // ServerTypeGetResponse defines the schema of the response when // retrieving a single server type. type ServerTypeGetResponse struct { ServerType ServerType `json:"server_type"` } hcloud-go-1.17.0/hcloud/schema/ssh_key.go000066400000000000000000000027301356226054300202030ustar00rootroot00000000000000package schema import "time" // SSHKey defines the schema of a SSH key. type SSHKey struct { ID int `json:"id"` Name string `json:"name"` Fingerprint string `json:"fingerprint"` PublicKey string `json:"public_key"` Labels map[string]string `json:"labels"` Created time.Time `json:"created"` } // SSHKeyCreateRequest defines the schema of the request // to create a SSH key. type SSHKeyCreateRequest struct { Name string `json:"name"` PublicKey string `json:"public_key"` Labels *map[string]string `json:"labels,omitempty"` } // SSHKeyCreateResponse defines the schema of the response // when creating a SSH key. type SSHKeyCreateResponse struct { SSHKey SSHKey `json:"ssh_key"` } // SSHKeyListResponse defines the schema of the response // when listing SSH keys. type SSHKeyListResponse struct { SSHKeys []SSHKey `json:"ssh_keys"` } // SSHKeyGetResponse defines the schema of the response // when retrieving a single SSH key. type SSHKeyGetResponse struct { SSHKey SSHKey `json:"ssh_key"` } // SSHKeyUpdateRequest defines the schema of the request to update a SSH key. type SSHKeyUpdateRequest struct { Name string `json:"name,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // SSHKeyUpdateResponse defines the schema of the response when updating a SSH key. type SSHKeyUpdateResponse struct { SSHKey SSHKey `json:"ssh_key"` } hcloud-go-1.17.0/hcloud/schema/ssh_key_test.go000066400000000000000000000024441356226054300212440ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestSSHKeyCreateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in SSHKeyCreateRequest out []byte }{ { name: "no labels", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key"}, out: []byte(`{"name":"test","public_key":"key"}`), }, { name: "one label", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &oneLabel}, out: []byte(`{"name":"test","public_key":"key","labels":{"foo":"bar"}}`), }, { name: "nil labels", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &nilLabels}, out: []byte(`{"name":"test","public_key":"key","labels":null}`), }, { name: "empty labels", in: SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &emptyLabels}, out: []byte(`{"name":"test","public_key":"key","labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } hcloud-go-1.17.0/hcloud/schema/volume.go000066400000000000000000000070671356226054300200550ustar00rootroot00000000000000package schema import "time" // Volume defines the schema of a volume. type Volume struct { ID int `json:"id"` Name string `json:"name"` Server *int `json:"server"` Location Location `json:"location"` Size int `json:"size"` Protection VolumeProtection `json:"protection"` Labels map[string]string `json:"labels"` LinuxDevice string `json:"linux_device"` Created time.Time `json:"created"` } // VolumeCreateRequest defines the schema of the request // to create a volume. type VolumeCreateRequest struct { Name string `json:"name"` Size int `json:"size"` Server *int `json:"server,omitempty"` Location interface{} `json:"location,omitempty"` // int, string, or nil Labels *map[string]string `json:"labels,omitempty"` Automount *bool `json:"automount,omitempty"` Format *string `json:"format,omitempty"` } // VolumeCreateResponse defines the schema of the response // when creating a volume. type VolumeCreateResponse struct { Volume Volume `json:"volume"` Action *Action `json:"action"` NextActions []Action `json:"next_actions"` } // VolumeListResponse defines the schema of the response // when listing volumes. type VolumeListResponse struct { Volumes []Volume `json:"volumes"` } // VolumeGetResponse defines the schema of the response // when retrieving a single volume. type VolumeGetResponse struct { Volume Volume `json:"volume"` } // VolumeUpdateRequest defines the schema of the request to update a volume. type VolumeUpdateRequest struct { Name string `json:"name,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } // VolumeUpdateResponse defines the schema of the response when updating a volume. type VolumeUpdateResponse struct { Volume Volume `json:"volume"` } // VolumeProtection defines the schema of a volume's resource protection. type VolumeProtection struct { Delete bool `json:"delete"` } // VolumeActionChangeProtectionRequest defines the schema of the request to // change the resource protection of a volume. type VolumeActionChangeProtectionRequest struct { Delete *bool `json:"delete,omitempty"` } // VolumeActionChangeProtectionResponse defines the schema of the response when // changing the resource protection of a volume. type VolumeActionChangeProtectionResponse struct { Action Action `json:"action"` } // VolumeActionAttachVolumeRequest defines the schema of the request to // attach a volume to a server. type VolumeActionAttachVolumeRequest struct { Server int `json:"server"` Automount *bool `json:"automount,omitempty"` } // VolumeActionAttachVolumeResponse defines the schema of the response when // attaching a volume to a server. type VolumeActionAttachVolumeResponse struct { Action Action `json:"action"` } // VolumeActionDetachVolumeRequest defines the schema of the request to // create an detach volume action. type VolumeActionDetachVolumeRequest struct{} // VolumeActionDetachVolumeResponse defines the schema of the response when // creating an detach volume action. type VolumeActionDetachVolumeResponse struct { Action Action `json:"action"` } // VolumeActionResizeVolumeRequest defines the schema of the request to resize a volume. type VolumeActionResizeVolumeRequest struct { Size int `json:"size"` } // VolumeActionResizeVolumeResponse defines the schema of the response when resizing a volume. type VolumeActionResizeVolumeResponse struct { Action Action `json:"action"` } hcloud-go-1.17.0/hcloud/schema/volume_test.go000066400000000000000000000020431356226054300211010ustar00rootroot00000000000000package schema import ( "bytes" "encoding/json" "testing" ) func TestVolumeUpdateRequest(t *testing.T) { var ( oneLabel = map[string]string{"foo": "bar"} nilLabels map[string]string emptyLabels = map[string]string{} ) testCases := []struct { name string in VolumeUpdateRequest out []byte }{ { name: "no labels", in: VolumeUpdateRequest{}, out: []byte(`{}`), }, { name: "one label", in: VolumeUpdateRequest{Labels: &oneLabel}, out: []byte(`{"labels":{"foo":"bar"}}`), }, { name: "nil labels", in: VolumeUpdateRequest{Labels: &nilLabels}, out: []byte(`{"labels":null}`), }, { name: "empty labels", in: VolumeUpdateRequest{Labels: &emptyLabels}, out: []byte(`{"labels":{}}`), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { data, err := json.Marshal(testCase.in) if err != nil { t.Fatal(err) } if !bytes.Equal(data, testCase.out) { t.Fatalf("output %s does not match %s", data, testCase.out) } }) } } hcloud-go-1.17.0/hcloud/schema_test.go000066400000000000000000001021461356226054300175770ustar00rootroot00000000000000package hcloud import ( "encoding/json" "testing" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestActionFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "command": "create_server", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00Z", "finished": "2016-01-30T23:56:13Z", "resources": [ { "id": 42, "type": "server" } ], "error": { "code": "action_failed", "message": "Action failed" } }`) var s schema.Action if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } action := ActionFromSchema(s) if action.ID != 1 { t.Errorf("unexpected ID: %v", action.ID) } if action.Command != "create_server" { t.Errorf("unexpected command: %v", action.Command) } if action.Status != "success" { t.Errorf("unexpected status: %v", action.Status) } if action.Progress != 100 { t.Errorf("unexpected progress: %d", action.Progress) } if !action.Started.Equal(time.Date(2016, 1, 30, 23, 55, 0, 0, time.UTC)) { t.Errorf("unexpected started: %v", action.Started) } if !action.Finished.Equal(time.Date(2016, 1, 30, 23, 56, 13, 0, time.UTC)) { t.Errorf("unexpected finished: %v", action.Started) } if action.ErrorCode != "action_failed" { t.Errorf("unexpected error code: %v", action.ErrorCode) } if action.ErrorMessage != "Action failed" { t.Errorf("unexpected error message: %v", action.ErrorMessage) } if len(action.Resources) == 1 { if action.Resources[0].ID != 42 { t.Errorf("unexpected id in resources[0].ID: %v", action.Resources[0].ID) } if action.Resources[0].Type != ActionResourceTypeServer { t.Errorf("unexpected type in resources[0].Type: %v", action.Resources[0].Type) } } else { t.Errorf("unexpected number of resources") } } func TestActionsFromSchema(t *testing.T) { data := []byte(`[ { "id": 13, "command": "create_server" }, { "id": 14, "command": "start_server" } ]`) var s []schema.Action if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } actions := ActionsFromSchema(s) if len(actions) != 2 || actions[0].ID != 13 || actions[1].ID != 14 { t.Fatal("unexpected actions") } } func TestFloatingIPFromSchema(t *testing.T) { t.Run("IPv6", func(t *testing.T) { data := []byte(`{ "id": 4711, "name": "Web Frontend", "description": "Web Frontend", "created":"2017-08-16T17:29:14+00:00", "ip": "2001:db8::/64", "type": "ipv6", "server": null, "dns_ptr": [], "blocked": true, "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central" }, "protection": { "delete": true }, "labels": { "key": "value", "key2": "value2" } }`) var s schema.FloatingIP if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } floatingIP := FloatingIPFromSchema(s) if floatingIP.ID != 4711 { t.Errorf("unexpected ID: %v", floatingIP.ID) } if !floatingIP.Blocked { t.Errorf("unexpected value for Blocked: %v", floatingIP.Blocked) } if floatingIP.Name != "Web Frontend" { t.Errorf("unexpected name: %v", floatingIP.Name) } if floatingIP.Description != "Web Frontend" { t.Errorf("unexpected description: %v", floatingIP.Description) } if floatingIP.IP.String() != "2001:db8::" { t.Errorf("unexpected IP: %v", floatingIP.IP) } if floatingIP.Type != FloatingIPTypeIPv6 { t.Errorf("unexpected Type: %v", floatingIP.Type) } if floatingIP.Server != nil { t.Errorf("unexpected Server: %v", floatingIP.Server) } if floatingIP.DNSPtr == nil || floatingIP.DNSPtrForIP(floatingIP.IP) != "" { t.Errorf("unexpected DNS ptr: %v", floatingIP.DNSPtr) } if floatingIP.HomeLocation == nil || floatingIP.HomeLocation.ID != 1 { t.Errorf("unexpected home location: %v", floatingIP.HomeLocation) } if !floatingIP.Protection.Delete { t.Errorf("unexpected Protection.Delete: %v", floatingIP.Protection.Delete) } if floatingIP.Labels["key"] != "value" || floatingIP.Labels["key2"] != "value2" { t.Errorf("unexpected Labels: %v", floatingIP.Labels) } if !floatingIP.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", floatingIP.Created) } }) t.Run("IPv4", func(t *testing.T) { data := []byte(`{ "id": 4711, "description": "Web Frontend", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{ "ip": "131.232.99.1", "dns_ptr": "fip01.example.com" }], "blocked": false, "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 } }`) var s schema.FloatingIP if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } floatingIP := FloatingIPFromSchema(s) if floatingIP.ID != 4711 { t.Errorf("unexpected ID: %v", floatingIP.ID) } if floatingIP.Blocked { t.Errorf("unexpected value for Blocked: %v", floatingIP.Blocked) } if floatingIP.Description != "Web Frontend" { t.Errorf("unexpected description: %v", floatingIP.Description) } if floatingIP.IP.String() != "131.232.99.1" { t.Errorf("unexpected IP: %v", floatingIP.IP) } if floatingIP.Type != FloatingIPTypeIPv4 { t.Errorf("unexpected type: %v", floatingIP.Type) } if floatingIP.Server == nil || floatingIP.Server.ID != 42 { t.Errorf("unexpected server: %v", floatingIP.Server) } if floatingIP.DNSPtr == nil || floatingIP.DNSPtrForIP(floatingIP.IP) != "fip01.example.com" { t.Errorf("unexpected DNS ptr: %v", floatingIP.DNSPtr) } if floatingIP.HomeLocation == nil || floatingIP.HomeLocation.ID != 1 { t.Errorf("unexpected home location: %v", floatingIP.HomeLocation) } }) } func TestISOFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00" }`) var s schema.ISO if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } iso := ISOFromSchema(s) if iso.ID != 4711 { t.Errorf("unexpected ID: %v", iso.ID) } if iso.Name != "FreeBSD-11.0-RELEASE-amd64-dvd1" { t.Errorf("unexpected name: %v", iso.Name) } if iso.Description != "FreeBSD 11.0 x64" { t.Errorf("unexpected description: %v", iso.Description) } if iso.Type != ISOTypePublic { t.Errorf("unexpected type: %v", iso.Type) } if iso.Deprecated.IsZero() { t.Errorf("unexpected value for deprecated: %v", iso.Deprecated) } } func TestDatacenterFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central" }, "server_types": { "supported": [ 1, 1, 2, 3 ], "available": [ 1, 1, 2, 3 ] } }`) var s schema.Datacenter if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } datacenter := DatacenterFromSchema(s) if datacenter.ID != 1 { t.Errorf("unexpected ID: %v", datacenter.ID) } if datacenter.Name != "fsn1-dc8" { t.Errorf("unexpected Name: %v", datacenter.Name) } if datacenter.Location == nil || datacenter.Location.ID != 1 { t.Errorf("unexpected Location: %v", datacenter.Location) } if len(datacenter.ServerTypes.Available) != 4 { t.Errorf("unexpected ServerTypes.Available (should be 4): %v", len(datacenter.ServerTypes.Available)) } if len(datacenter.ServerTypes.Supported) != 4 { t.Errorf("unexpected ServerTypes.Supported length (should be 4): %v", len(datacenter.ServerTypes.Supported)) } } func TestLocationFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central" }`) var s schema.Location if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } location := LocationFromSchema(s) if location.ID != 1 { t.Errorf("unexpected ID: %v", location.ID) } if location.Name != "fsn1" { t.Errorf("unexpected Name: %v", location.Name) } if location.Description != "Falkenstein DC Park 1" { t.Errorf("unexpected Description: %v", location.Description) } if location.Country != "DE" { t.Errorf("unexpected Country: %v", location.Country) } if location.City != "Falkenstein" { t.Errorf("unexpected City: %v", location.City) } if location.Latitude != 50.47612 { t.Errorf("unexpected Latitude: %v", location.Latitude) } if location.Longitude != 12.370071 { t.Errorf("unexpected Longitude: %v", location.Longitude) } if location.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", location.NetworkZone) } } func TestServerFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "server.example.com", "status": "running", "created": "2017-08-16T17:29:14+00:00", "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": false, "dns_ptr": "server01.example.com" }, "ipv6": { "ip": "2a01:4f8:1c11:3400::/64", "blocked": false, "dns_ptr": [ { "ip": "2a01:4f8:1c11:3400::1/64", "dns_ptr": "server01.example.com" } ] } }, "private_net": [ { "network": 4711, "ip": "10.0.1.1", "aliases": [ "10.0.1.2" ] } ], "server_type": { "id": 2 }, "outgoing_traffic": 123456, "ingoing_traffic": 7891011, "included_traffic": 654321, "backup_window": "22-02", "rescue_enabled": true, "image": { "id": 4711, "type": "system", "status": "available", "name": "ubuntu16.04-standard-x64", "description": "Ubuntu 16.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2017-08-16T17:29:14+00:00", "created_from": { "id": 1, "name": "Server" }, "bound_to": 1, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": false }, "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public" }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central" } }, "protection": { "delete": true, "rebuild": true }, "locked": true, "labels": { "key": "value", "key2": "value2" }, "volumes": [123, 456, 789] }`) var s schema.Server if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } server := ServerFromSchema(s) if server.ID != 1 { t.Errorf("unexpected ID: %v", server.ID) } if server.Name != "server.example.com" { t.Errorf("unexpected name: %v", server.Name) } if server.Status != ServerStatusRunning { t.Errorf("unexpected status: %v", server.Status) } if !server.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", server.Created) } if server.PublicNet.IPv4.IP.String() != "1.2.3.4" { t.Errorf("unexpected public net IPv4 IP: %v", server.PublicNet.IPv4.IP) } if server.ServerType.ID != 2 { t.Errorf("unexpected server type ID: %v", server.ServerType.ID) } if server.IncludedTraffic != 654321 { t.Errorf("unexpected included traffic: %v", server.IncludedTraffic) } if server.OutgoingTraffic != 123456 { t.Errorf("unexpected outgoing traffic: %v", server.OutgoingTraffic) } if server.IngoingTraffic != 7891011 { t.Errorf("unexpected ingoing traffic: %v", server.IngoingTraffic) } if server.BackupWindow != "22-02" { t.Errorf("unexpected backup window: %v", server.BackupWindow) } if !server.RescueEnabled { t.Errorf("unexpected rescue enabled state: %v", server.RescueEnabled) } if server.Image == nil || server.Image.ID != 4711 { t.Errorf("unexpected Image: %v", server.Image) } if server.ISO == nil || server.ISO.ID != 4711 { t.Errorf("unexpected ISO: %v", server.ISO) } if server.Datacenter == nil || server.Datacenter.ID != 1 { t.Errorf("unexpected Datacenter: %v", server.Datacenter) } if !server.Locked { t.Errorf("unexpected value for Locked: %v", server.Locked) } if !server.Protection.Delete { t.Errorf("unexpected value for Protection.Delete: %v", server.Protection.Delete) } if !server.Protection.Rebuild { t.Errorf("unexpected value for Protection.Rebuild: %v", server.Protection.Rebuild) } if server.Labels["key"] != "value" || server.Labels["key2"] != "value2" { t.Errorf("unexpected Labels: %v", server.Labels) } if len(s.Volumes) != 3 { t.Errorf("unexpected number of volumes: %v", len(s.Volumes)) } if s.Volumes[0] != 123 || s.Volumes[1] != 456 || s.Volumes[2] != 789 { t.Errorf("unexpected volumes: %v", s.Volumes) } if len(server.PrivateNet) != 1 { t.Errorf("unexpected length of PrivateNet: %v", len(server.PrivateNet)) } if server.PrivateNet[0].Network.ID != 4711 { t.Errorf("unexpected first private net: %v", server.PrivateNet[0]) } } func TestServerFromSchemaNoTraffic(t *testing.T) { data := []byte(`{ "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": false, "dns_ptr": "server01.example.com" }, "ipv6": { "ip": "2a01:4f8:1c11:3400::/64", "blocked": false, "dns_ptr": [ { "ip": "2a01:4f8:1c11:3400::1/64", "dns_ptr": "server01.example.com" } ] } }, "outgoing_traffic": null, "ingoing_traffic": null }`) var s schema.Server if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } server := ServerFromSchema(s) if server.OutgoingTraffic != 0 { t.Errorf("unexpected outgoing traffic: %v", server.OutgoingTraffic) } if server.IngoingTraffic != 0 { t.Errorf("unexpected ingoing traffic: %v", server.IngoingTraffic) } } func TestServerPublicNetFromSchema(t *testing.T) { data := []byte(`{ "ipv4": { "ip": "1.2.3.4", "blocked": false, "dns_ptr": "server.example.com" }, "ipv6": { "ip": "2a01:4f8:1c19:1403::/64", "blocked": false, "dns_ptr": [] }, "floating_ips": [4] }`) var s schema.ServerPublicNet if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } publicNet := ServerPublicNetFromSchema(s) if publicNet.IPv4.IP.String() != "1.2.3.4" { t.Errorf("unexpected IPv4 IP: %v", publicNet.IPv4.IP) } if publicNet.IPv6.Network.String() != "2a01:4f8:1c19:1403::/64" { t.Errorf("unexpected IPv6 IP: %v", publicNet.IPv6.IP) } if len(publicNet.FloatingIPs) != 1 || publicNet.FloatingIPs[0].ID != 4 { t.Errorf("unexpected Floating IPs: %v", publicNet.FloatingIPs) } } func TestServerPublicNetIPv4FromSchema(t *testing.T) { data := []byte(`{ "ip": "1.2.3.4", "blocked": true, "dns_ptr": "server.example.com" }`) var s schema.ServerPublicNetIPv4 if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } ipv4 := ServerPublicNetIPv4FromSchema(s) if ipv4.IP.String() != "1.2.3.4" { t.Errorf("unexpected IP: %v", ipv4.IP) } if !ipv4.Blocked { t.Errorf("unexpected blocked state: %v", ipv4.Blocked) } if ipv4.DNSPtr != "server.example.com" { t.Errorf("unexpected DNS ptr: %v", ipv4.DNSPtr) } } func TestServerPublicNetIPv6FromSchema(t *testing.T) { data := []byte(`{ "ip": "2a01:4f8:1c11:3400::/64", "blocked": true, "dns_ptr": [ { "ip": "2a01:4f8:1c11:3400::1/64", "blocked": "server01.example.com" } ] }`) var s schema.ServerPublicNetIPv6 if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } ipv6 := ServerPublicNetIPv6FromSchema(s) if ipv6.Network.String() != "2a01:4f8:1c11:3400::/64" { t.Errorf("unexpected IP: %v", ipv6.IP) } if !ipv6.Blocked { t.Errorf("unexpected blocked state: %v", ipv6.Blocked) } if len(ipv6.DNSPtr) != 1 { t.Errorf("unexpected DNS ptr: %v", ipv6.DNSPtr) } } func TestServerPrivateNetFromSchema(t *testing.T) { data := []byte(`{ "network": 4711, "ip": "10.0.1.1", "alias_ips": [ "10.0.1.2" ], "mac_address": "86:00:ff:2a:7d:e1" }`) var s schema.ServerPrivateNet if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } privateNet := ServerPrivateNetFromSchema(s) if privateNet.Network.ID != 4711 { t.Errorf("unexpected Network: %v", privateNet.Network) } if privateNet.IP.String() != "10.0.1.1" { t.Errorf("unexpected IP: %v", privateNet.IP) } if len(privateNet.Aliases) != 1 { t.Errorf("unexpected number of alias IPs: %v", len(privateNet.Aliases)) } if privateNet.Aliases[0].String() != "10.0.1.2" { t.Errorf("unexpected alias IP: %v", privateNet.Aliases[0]) } if privateNet.MACAddress != "86:00:ff:2a:7d:e1" { t.Errorf("unexpected mac address: %v", privateNet.MACAddress) } } func TestServerTypeFromSchema(t *testing.T) { data := []byte(`{ "id": 1, "name": "cx10", "description": "description", "cores": 4, "memory": 1.0, "disk": 20, "storage_type": "local", "cpu_type": "shared", "prices": [ { "location": "fsn1", "price_hourly": { "net": "1", "gross": "1.19" }, "price_monthly": { "net": "1", "gross": "1.19" } } ] }`) var s schema.ServerType if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } serverType := ServerTypeFromSchema(s) if serverType.ID != 1 { t.Errorf("unexpected ID: %v", serverType.ID) } if serverType.Name != "cx10" { t.Errorf("unexpected name: %q", serverType.Name) } if serverType.Description != "description" { t.Errorf("unexpected description: %q", serverType.Description) } if serverType.Cores != 4 { t.Errorf("unexpected cores: %v", serverType.Cores) } if serverType.Memory != 1.0 { t.Errorf("unexpected memory: %v", serverType.Memory) } if serverType.Disk != 20 { t.Errorf("unexpected disk: %v", serverType.Disk) } if serverType.StorageType != StorageTypeLocal { t.Errorf("unexpected storage type: %q", serverType.StorageType) } if serverType.CPUType != CPUTypeShared { t.Errorf("unexpected cpu type: %q", serverType.CPUType) } if len(serverType.Pricings) != 1 { t.Errorf("unexpected number of pricings: %d", len(serverType.Pricings)) } else { if serverType.Pricings[0].Location.Name != "fsn1" { t.Errorf("unexpected location name: %v", serverType.Pricings[0].Location.Name) } if serverType.Pricings[0].Hourly.Net != "1" { t.Errorf("unexpected hourly net price: %v", serverType.Pricings[0].Hourly.Net) } if serverType.Pricings[0].Hourly.Gross != "1.19" { t.Errorf("unexpected hourly gross price: %v", serverType.Pricings[0].Hourly.Gross) } if serverType.Pricings[0].Monthly.Net != "1" { t.Errorf("unexpected monthly net price: %v", serverType.Pricings[0].Monthly.Net) } if serverType.Pricings[0].Monthly.Gross != "1.19" { t.Errorf("unexpected monthly gross price: %v", serverType.Pricings[0].Monthly.Gross) } } } func TestSSHKeyFromSchema(t *testing.T) { data := []byte(`{ "id": 2323, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": { "key": "value", "key2": "value2" }, "created":"2017-08-16T17:29:14+00:00" }`) var s schema.SSHKey if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } sshKey := SSHKeyFromSchema(s) if sshKey.ID != 2323 { t.Errorf("unexpected ID: %v", sshKey.ID) } if sshKey.Name != "My key" { t.Errorf("unexpected name: %v", sshKey.Name) } if sshKey.Fingerprint != "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c" { t.Errorf("unexpected fingerprint: %v", sshKey.Fingerprint) } if sshKey.PublicKey != "ssh-rsa AAAjjk76kgf...Xt" { t.Errorf("unexpected public key: %v", sshKey.PublicKey) } if sshKey.Labels["key"] != "value" || sshKey.Labels["key2"] != "value2" { t.Errorf("unexpected labels: %v", sshKey.Labels) } if !sshKey.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", sshKey.Created) } } func TestErrorFromSchema(t *testing.T) { t.Run("service_error", func(t *testing.T) { data := []byte(`{ "code": "service_error", "message": "An error occured", "details": {} }`) var s schema.Error if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } err := ErrorFromSchema(s) if err.Code != "service_error" { t.Errorf("unexpected code: %v", err.Code) } if err.Message != "An error occured" { t.Errorf("unexpected message: %v", err.Message) } }) t.Run("invalid_input", func(t *testing.T) { data := []byte(`{ "code": "invalid_input", "message": "invalid input", "details": { "fields": [ { "name": "broken_field", "messages": ["is required"] } ] } }`) var s schema.Error if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } err := ErrorFromSchema(s) if err.Code != "invalid_input" { t.Errorf("unexpected Code: %v", err.Code) } if err.Message != "invalid input" { t.Errorf("unexpected Message: %v", err.Message) } if d, ok := err.Details.(ErrorDetailsInvalidInput); !ok { t.Fatalf("unexpected Details type (should be ErrorDetailsInvalidInput): %v", err.Details) } else { if len(d.Fields) != 1 { t.Fatalf("unexpected Details.Fields length (should be 1): %v", d.Fields) } if d.Fields[0].Name != "broken_field" { t.Errorf("unexpected Details.Fields[0].Name: %v", d.Fields[0].Name) } if len(d.Fields[0].Messages) != 1 { t.Fatalf("unexpected Details.Fields[0].Messages length (should be 1): %v", d.Fields[0].Messages) } if d.Fields[0].Messages[0] != "is required" { t.Errorf("unexpected Details.Fields[0].Messages[0]: %v", d.Fields[0].Messages[0]) } } }) } func TestPaginationFromSchema(t *testing.T) { data := []byte(`{ "page": 2, "per_page": 25, "previous_page": 1, "next_page": 3, "last_page": 13, "total_entries": 322 }`) var s schema.MetaPagination if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } p := PaginationFromSchema(s) if p.Page != 2 { t.Errorf("unexpected page: %v", p.Page) } if p.PerPage != 25 { t.Errorf("unexpected per page: %v", p.PerPage) } if p.PreviousPage != 1 { t.Errorf("unexpected previous page: %v", p.PreviousPage) } if p.NextPage != 3 { t.Errorf("unexpected next page: %d", p.NextPage) } if p.LastPage != 13 { t.Errorf("unexpected last page: %d", p.LastPage) } if p.TotalEntries != 322 { t.Errorf("unexpected total entries: %d", p.TotalEntries) } } func TestImageFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "type": "system", "status": "available", "name": "ubuntu16.04-standard-x64", "description": "Ubuntu 16.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:55:01Z", "created_from": { "id": 1, "name": "my-server1" }, "bound_to": 1, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": false, "protection": { "delete": true }, "deprecated": "2018-02-28T00:00:00+00:00", "labels": { "key": "value", "key2": "value2" } }`) var s schema.Image if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } image := ImageFromSchema(s) if image.ID != 4711 { t.Errorf("unexpected ID: %v", image.ID) } if image.Type != ImageTypeSystem { t.Errorf("unexpected Type: %v", image.Type) } if image.Status != ImageStatusAvailable { t.Errorf("unexpected Status: %v", image.Status) } if image.Name != "ubuntu16.04-standard-x64" { t.Errorf("unexpected Name: %v", image.Name) } if image.Description != "Ubuntu 16.04 Standard 64 bit" { t.Errorf("unexpected Description: %v", image.Description) } if image.ImageSize != 2.3 { t.Errorf("unexpected ImageSize: %v", image.ImageSize) } if image.DiskSize != 10 { t.Errorf("unexpected DiskSize: %v", image.DiskSize) } if !image.Created.Equal(time.Date(2016, 1, 30, 23, 55, 1, 0, time.UTC)) { t.Errorf("unexpected Created: %v", image.Created) } if image.CreatedFrom == nil || image.CreatedFrom.ID != 1 || image.CreatedFrom.Name != "my-server1" { t.Errorf("unexpected CreatedFrom: %v", image.CreatedFrom) } if image.BoundTo == nil || image.BoundTo.ID != 1 { t.Errorf("unexpected BoundTo: %v", image.BoundTo) } if image.OSVersion != "16.04" { t.Errorf("unexpected OSVersion: %v", image.OSVersion) } if image.OSFlavor != "ubuntu" { t.Errorf("unexpected OSFlavor: %v", image.OSFlavor) } if image.RapidDeploy { t.Errorf("unexpected RapidDeploy: %v", image.RapidDeploy) } if !image.Protection.Delete { t.Errorf("unexpected Protection.Delete: %v", image.Protection.Delete) } if image.Deprecated.IsZero() { t.Errorf("unexpected value for Deprecated: %v", image.Deprecated) } if image.Labels["key"] != "value" || image.Labels["key2"] != "value2" { t.Errorf("unexpected Labels: %v", image.Labels) } } func TestVolumeFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "created": "2016-01-30T23:50:11+00:00", "name": "db-storage", "server": 2, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": { "key": "value", "key2": "value2" } }`) var s schema.Volume if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } volume := VolumeFromSchema(s) if volume.ID != 4711 { t.Errorf("unexpected ID: %v", volume.ID) } if volume.Name != "db-storage" { t.Errorf("unexpected name: %v", volume.Name) } if !volume.Created.Equal(time.Date(2016, 1, 30, 23, 50, 11, 0, time.UTC)) { t.Errorf("unexpected created date: %s", volume.Created) } if volume.Server == nil { t.Error("no server") } if volume.Server != nil && volume.Server.ID != 2 { t.Errorf("unexpected server ID: %v", volume.Server.ID) } if volume.Location == nil || volume.Location.ID != 1 { t.Errorf("unexpected location: %v", volume.Location) } if volume.Size != 42 { t.Errorf("unexpected size: %v", volume.Size) } if !volume.Protection.Delete { t.Errorf("unexpected value for delete protection: %v", volume.Protection.Delete) } if len(volume.Labels) != 2 { t.Errorf("unexpected number of labels: %d", len(volume.Labels)) } if volume.Labels["key"] != "value" || volume.Labels["key2"] != "value2" { t.Errorf("unexpected labels: %v", volume.Labels) } } func TestNetworkFromSchema(t *testing.T) { data := []byte(`{ "id": 4711, "name": "mynet", "created": "2017-08-16T17:29:14+00:00", "ip_range": "10.0.0.0/16", "subnets": [ { "type": "server", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1" } ], "routes": [ { "destination": "10.100.1.0/24", "gateway": "10.0.1.1" } ], "servers": [ 4711 ], "protection": { "delete": false }, "labels": {} }`) var s schema.Network if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } network := NetworkFromSchema(s) if network.ID != 4711 { t.Errorf("unexpected ID: %v", network.ID) } if network.Name != "mynet" { t.Errorf("unexpected Name: %v", network.Name) } if !network.Created.Equal(time.Date(2017, 8, 16, 17, 29, 14, 0, time.UTC)) { t.Errorf("unexpected created date: %v", network.Created) } if network.IPRange.String() != "10.0.0.0/16" { t.Errorf("unexpected IPRange: %v", network.IPRange) } if len(network.Subnets) != 1 { t.Errorf("unexpected length of Subnets: %v", len(network.Subnets)) } if len(network.Routes) != 1 { t.Errorf("unexpected length of Routes: %v", len(network.Routes)) } if len(network.Servers) != 1 { t.Errorf("unexpected length of Servers: %v", len(network.Servers)) } if network.Servers[0].ID != 4711 { t.Errorf("unexpected Server ID: %v", network.Servers[0].ID) } if network.Protection.Delete { t.Errorf("unexpected value for delete protection: %v", network.Protection.Delete) } } func TestNetworkSubnetFromSchema(t *testing.T) { t.Run("type server", func(t *testing.T) { data := []byte(`{ "type": "server", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1" }`) var s schema.NetworkSubnet if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } networkSubnet := NetworkSubnetFromSchema(s) if networkSubnet.NetworkZone != "eu-central" { t.Errorf("unexpected NetworkZone: %v", networkSubnet.NetworkZone) } if networkSubnet.Type != "server" { t.Errorf("unexpected Type: %v", networkSubnet.Type) } if networkSubnet.IPRange.String() != "10.0.1.0/24" { t.Errorf("unexpected IPRange: %v", networkSubnet.IPRange) } if networkSubnet.Gateway.String() != "10.0.0.1" { t.Errorf("unexpected Gateway: %v", networkSubnet.Gateway) } }) } func TestNetworkRouteFromSchema(t *testing.T) { data := []byte(`{ "destination": "10.100.1.0/24", "gateway": "10.0.1.1" }`) var s schema.NetworkRoute if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } networkRoute := NetworkRouteFromSchema(s) if networkRoute.Destination.String() != "10.100.1.0/24" { t.Errorf("unexpected Destination: %v", networkRoute.Destination) } if networkRoute.Gateway.String() != "10.0.1.1" { t.Errorf("unexpected Gateway: %v", networkRoute.Gateway) } } func TestPricingFromSchema(t *testing.T) { data := []byte(`{ "currency": "EUR", "vat_rate": "19.00", "image": { "price_per_gb_month": { "net": "1", "gross": "1.19" } }, "floating_ip": { "price_monthly": { "net": "1", "gross": "1.19" } }, "traffic": { "price_per_tb": { "net": "1", "gross": "1.19" } }, "server_backup": { "percentage": "20" }, "server_types": [ { "id": 4, "name": "CX11", "prices": [ { "location": "fsn1", "price_hourly": { "net": "1", "gross": "1.19" }, "price_monthly": { "net": "1", "gross": "1.19" } } ] } ] }`) var s schema.Pricing if err := json.Unmarshal(data, &s); err != nil { t.Fatal(err) } pricing := PricingFromSchema(s) if pricing.Image.PerGBMonth.Currency != "EUR" { t.Errorf("unexpected Image.PerGBMonth.Currency: %v", pricing.Image.PerGBMonth.Currency) } if pricing.Image.PerGBMonth.VATRate != "19.00" { t.Errorf("unexpected Image.PerGBMonth.VATRate: %v", pricing.Image.PerGBMonth.VATRate) } if pricing.Image.PerGBMonth.Net != "1" { t.Errorf("unexpected Image.PerGBMonth.Net: %v", pricing.Image.PerGBMonth.Net) } if pricing.Image.PerGBMonth.Gross != "1.19" { t.Errorf("unexpected Image.PerGBMonth.Gross: %v", pricing.Image.PerGBMonth.Gross) } if pricing.FloatingIP.Monthly.Currency != "EUR" { t.Errorf("unexpected FloatingIP.Monthly.Currency: %v", pricing.FloatingIP.Monthly.Currency) } if pricing.FloatingIP.Monthly.VATRate != "19.00" { t.Errorf("unexpected FloatingIP.Monthly.VATRate: %v", pricing.FloatingIP.Monthly.VATRate) } if pricing.FloatingIP.Monthly.Net != "1" { t.Errorf("unexpected FloatingIP.Monthly.Net: %v", pricing.FloatingIP.Monthly.Net) } if pricing.FloatingIP.Monthly.Gross != "1.19" { t.Errorf("unexpected FloatingIP.Monthly.Gross: %v", pricing.FloatingIP.Monthly.Gross) } if pricing.Traffic.PerTB.Currency != "EUR" { t.Errorf("unexpected Traffic.PerTB.Currency: %v", pricing.Traffic.PerTB.Currency) } if pricing.Traffic.PerTB.VATRate != "19.00" { t.Errorf("unexpected Traffic.PerTB.VATRate: %v", pricing.Traffic.PerTB.VATRate) } if pricing.Traffic.PerTB.Net != "1" { t.Errorf("unexpected Traffic.PerTB.Net: %v", pricing.Traffic.PerTB.Net) } if pricing.Traffic.PerTB.Gross != "1.19" { t.Errorf("unexpected Traffic.PerTB.Gross: %v", pricing.Traffic.PerTB.Gross) } if pricing.ServerBackup.Percentage != "20" { t.Errorf("unexpected ServerBackup.Percentage: %v", pricing.ServerBackup.Percentage) } if len(pricing.ServerTypes) != 1 { t.Errorf("unexpected number of server types: %d", len(pricing.ServerTypes)) } else { p := pricing.ServerTypes[0] if p.ServerType.ID != 4 { t.Errorf("unexpected ServerType.ID: %d", p.ServerType.ID) } if p.ServerType.Name != "CX11" { t.Errorf("unexpected ServerType.Name: %v", p.ServerType.Name) } if len(p.Pricings) != 1 { t.Errorf("unexpected number of prices: %d", len(p.Pricings)) } else { if p.Pricings[0].Location.Name != "fsn1" { t.Errorf("unexpected Location.Name: %v", p.Pricings[0].Location.Name) } if p.Pricings[0].Hourly.Currency != "EUR" { t.Errorf("unexpected Hourly.Currency: %v", p.Pricings[0].Hourly.Currency) } if p.Pricings[0].Hourly.VATRate != "19.00" { t.Errorf("unexpected Hourly.VATRate: %v", p.Pricings[0].Hourly.VATRate) } if p.Pricings[0].Hourly.Net != "1" { t.Errorf("unexpected Hourly.Net: %v", p.Pricings[0].Hourly.Net) } if p.Pricings[0].Hourly.Gross != "1.19" { t.Errorf("unexpected Hourly.Gross: %v", p.Pricings[0].Hourly.Gross) } if p.Pricings[0].Monthly.Currency != "EUR" { t.Errorf("unexpected Monthly.Currency: %v", p.Pricings[0].Monthly.Currency) } if p.Pricings[0].Monthly.VATRate != "19.00" { t.Errorf("unexpected Monthly.VATRate: %v", p.Pricings[0].Monthly.VATRate) } if p.Pricings[0].Monthly.Net != "1" { t.Errorf("unexpected Monthly.Net: %v", p.Pricings[0].Monthly.Net) } if p.Pricings[0].Monthly.Gross != "1.19" { t.Errorf("unexpected Monthly.Gross: %v", p.Pricings[0].Monthly.Gross) } } } } hcloud-go-1.17.0/hcloud/server.go000066400000000000000000000647531356226054300166210ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Server represents a server in the Hetzner Cloud. type Server struct { ID int Name string Status ServerStatus Created time.Time PublicNet ServerPublicNet PrivateNet []ServerPrivateNet ServerType *ServerType Datacenter *Datacenter IncludedTraffic uint64 OutgoingTraffic uint64 IngoingTraffic uint64 BackupWindow string RescueEnabled bool Locked bool ISO *ISO Image *Image Protection ServerProtection Labels map[string]string Volumes []*Volume } // ServerProtection represents the protection level of a server. type ServerProtection struct { Delete, Rebuild bool } // ServerStatus specifies a server's status. type ServerStatus string const ( // ServerStatusInitializing is the status when a server is initializing. ServerStatusInitializing ServerStatus = "initializing" // ServerStatusOff is the status when a server is off. ServerStatusOff ServerStatus = "off" // ServerStatusRunning is the status when a server is running. ServerStatusRunning ServerStatus = "running" // ServerStatusStarting is the status when a server is being started. ServerStatusStarting ServerStatus = "starting" // ServerStatusStopping is the status when a server is being stopped. ServerStatusStopping ServerStatus = "stopping" // ServerStatusMigrating is the status when a server is being migrated. ServerStatusMigrating ServerStatus = "migrating" // ServerStatusRebuilding is the status when a server is being rebuilt. ServerStatusRebuilding ServerStatus = "rebuilding" // ServerStatusDeleting is the status when a server is being deleted. ServerStatusDeleting ServerStatus = "deleting" // ServerStatusUnknown is the status when a server's state is unknown. ServerStatusUnknown ServerStatus = "unknown" ) // ServerPublicNet represents a server's public network. type ServerPublicNet struct { IPv4 ServerPublicNetIPv4 IPv6 ServerPublicNetIPv6 FloatingIPs []*FloatingIP } // ServerPublicNetIPv4 represents a server's public IPv4 address. type ServerPublicNetIPv4 struct { IP net.IP Blocked bool DNSPtr string } // ServerPublicNetIPv6 represents a server's public IPv6 network and address. type ServerPublicNetIPv6 struct { IP net.IP Network *net.IPNet Blocked bool DNSPtr map[string]string } // ServerPrivateNet defines the schema of a server's private network information. type ServerPrivateNet struct { Network *Network IP net.IP Aliases []net.IP MACAddress string } // DNSPtrForIP returns the reverse dns pointer of the ip address. func (s *ServerPublicNetIPv6) DNSPtrForIP(ip net.IP) string { return s.DNSPtr[ip.String()] } // ServerRescueType represents rescue types. type ServerRescueType string // List of rescue types. const ( ServerRescueTypeLinux32 ServerRescueType = "linux32" ServerRescueTypeLinux64 ServerRescueType = "linux64" ServerRescueTypeFreeBSD64 ServerRescueType = "freebsd64" ) // ServerClient is a client for the servers API. type ServerClient struct { client *Client } // GetByID retrieves a server by its ID. If the server does not exist, nil is returned. func (c *ServerClient) GetByID(ctx context.Context, id int) (*Server, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/servers/%d", id), nil) if err != nil { return nil, nil, err } var body schema.ServerGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return ServerFromSchema(body.Server), resp, nil } // GetByName retrieves a server by its name. If the server does not exist, nil is returned. func (c *ServerClient) GetByName(ctx context.Context, name string) (*Server, *Response, error) { servers, response, err := c.List(ctx, ServerListOpts{Name: name}) if len(servers) == 0 { return nil, response, err } return servers[0], response, err } // Get retrieves a server by its ID if the input can be parsed as an integer, otherwise it // retrieves a server by its name. If the server does not exist, nil is returned. func (c *ServerClient) Get(ctx context.Context, idOrName string) (*Server, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // ServerListOpts specifies options for listing servers. type ServerListOpts struct { ListOpts Name string Status []ServerStatus } func (l ServerListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } for _, status := range l.Status { vals.Add("status", string(status)) } return vals } // List returns a list of servers for a specific page. func (c *ServerClient) List(ctx context.Context, opts ServerListOpts) ([]*Server, *Response, error) { path := "/servers?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.ServerListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } servers := make([]*Server, 0, len(body.Servers)) for _, s := range body.Servers { servers = append(servers, ServerFromSchema(s)) } return servers, resp, nil } // All returns all servers. func (c *ServerClient) All(ctx context.Context) ([]*Server, error) { return c.AllWithOpts(ctx, ServerListOpts{ListOpts: ListOpts{PerPage: 50}}) } // AllWithOpts returns all servers for the given options. func (c *ServerClient) AllWithOpts(ctx context.Context, opts ServerListOpts) ([]*Server, error) { allServers := []*Server{} _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page servers, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allServers = append(allServers, servers...) return resp, nil }) if err != nil { return nil, err } return allServers, nil } // ServerCreateOpts specifies options for creating a new server. type ServerCreateOpts struct { Name string ServerType *ServerType Image *Image SSHKeys []*SSHKey Location *Location Datacenter *Datacenter UserData string StartAfterCreate *bool Labels map[string]string Automount *bool Volumes []*Volume Networks []*Network } // Validate checks if options are valid. func (o ServerCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } if o.ServerType == nil || (o.ServerType.ID == 0 && o.ServerType.Name == "") { return errors.New("missing server type") } if o.Image == nil || (o.Image.ID == 0 && o.Image.Name == "") { return errors.New("missing image") } if o.Location != nil && o.Datacenter != nil { return errors.New("location and datacenter are mutually exclusive") } return nil } // ServerCreateResult is the result of a create server call. type ServerCreateResult struct { Server *Server Action *Action RootPassword string NextActions []*Action } // Create creates a new server. func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (ServerCreateResult, *Response, error) { if err := opts.Validate(); err != nil { return ServerCreateResult{}, nil, err } var reqBody schema.ServerCreateRequest reqBody.UserData = opts.UserData reqBody.Name = opts.Name reqBody.Automount = opts.Automount reqBody.StartAfterCreate = opts.StartAfterCreate if opts.ServerType.ID != 0 { reqBody.ServerType = opts.ServerType.ID } else if opts.ServerType.Name != "" { reqBody.ServerType = opts.ServerType.Name } if opts.Image.ID != 0 { reqBody.Image = opts.Image.ID } else if opts.Image.Name != "" { reqBody.Image = opts.Image.Name } if opts.Labels != nil { reqBody.Labels = &opts.Labels } for _, sshKey := range opts.SSHKeys { reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID) } for _, volume := range opts.Volumes { reqBody.Volumes = append(reqBody.Volumes, volume.ID) } for _, network := range opts.Networks { reqBody.Networks = append(reqBody.Networks, network.ID) } if opts.Location != nil { if opts.Location.ID != 0 { reqBody.Location = strconv.Itoa(opts.Location.ID) } else { reqBody.Location = opts.Location.Name } } if opts.Datacenter != nil { if opts.Datacenter.ID != 0 { reqBody.Datacenter = strconv.Itoa(opts.Datacenter.ID) } else { reqBody.Datacenter = opts.Datacenter.Name } } reqBodyData, err := json.Marshal(reqBody) if err != nil { return ServerCreateResult{}, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/servers", bytes.NewReader(reqBodyData)) if err != nil { return ServerCreateResult{}, nil, err } var respBody schema.ServerCreateResponse resp, err := c.client.Do(req, &respBody) if err != nil { return ServerCreateResult{}, resp, err } result := ServerCreateResult{ Server: ServerFromSchema(respBody.Server), Action: ActionFromSchema(respBody.Action), NextActions: ActionsFromSchema(respBody.NextActions), } if respBody.RootPassword != nil { result.RootPassword = *respBody.RootPassword } return result, resp, nil } // Delete deletes a server. func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/servers/%d", server.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } // ServerUpdateOpts specifies options for updating a server. type ServerUpdateOpts struct { Name string Labels map[string]string } // Update updates a server. func (c *ServerClient) Update(ctx context.Context, server *Server, opts ServerUpdateOpts) (*Server, *Response, error) { reqBody := schema.ServerUpdateRequest{ Name: opts.Name, } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d", server.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ServerFromSchema(respBody.Server), resp, nil } // Poweron starts a server. func (c *ServerClient) Poweron(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/poweron", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionPoweronResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // Reboot reboots a server. func (c *ServerClient) Reboot(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/reboot", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionRebootResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // Reset resets a server. func (c *ServerClient) Reset(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/reset", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionResetResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // Shutdown shuts down a server. func (c *ServerClient) Shutdown(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/shutdown", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionShutdownResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // Poweroff stops a server. func (c *ServerClient) Poweroff(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/poweroff", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionPoweroffResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // ServerResetPasswordResult is the result of resetting a server's password. type ServerResetPasswordResult struct { Action *Action RootPassword string } // ResetPassword resets a server's password. func (c *ServerClient) ResetPassword(ctx context.Context, server *Server) (ServerResetPasswordResult, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/reset_password", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return ServerResetPasswordResult{}, nil, err } respBody := schema.ServerActionResetPasswordResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return ServerResetPasswordResult{}, resp, err } return ServerResetPasswordResult{ Action: ActionFromSchema(respBody.Action), RootPassword: respBody.RootPassword, }, resp, nil } // ServerCreateImageOpts specifies options for creating an image from a server. type ServerCreateImageOpts struct { Type ImageType Description *string Labels map[string]string } // Validate checks if options are valid. func (o ServerCreateImageOpts) Validate() error { switch o.Type { case ImageTypeSnapshot, ImageTypeBackup: break case "": break default: return errors.New("invalid type") } return nil } // ServerCreateImageResult is the result of creating an image from a server. type ServerCreateImageResult struct { Action *Action Image *Image } // CreateImage creates an image from a server. func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *ServerCreateImageOpts) (ServerCreateImageResult, *Response, error) { var reqBody schema.ServerActionCreateImageRequest if opts != nil { if err := opts.Validate(); err != nil { return ServerCreateImageResult{}, nil, fmt.Errorf("invalid options: %s", err) } if opts.Description != nil { reqBody.Description = opts.Description } if opts.Type != "" { reqBody.Type = String(string(opts.Type)) } if opts.Labels != nil { reqBody.Labels = &opts.Labels } } reqBodyData, err := json.Marshal(reqBody) if err != nil { return ServerCreateImageResult{}, nil, err } path := fmt.Sprintf("/servers/%d/actions/create_image", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return ServerCreateImageResult{}, nil, err } respBody := schema.ServerActionCreateImageResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return ServerCreateImageResult{}, resp, err } return ServerCreateImageResult{ Action: ActionFromSchema(respBody.Action), Image: ImageFromSchema(respBody.Image), }, resp, nil } // ServerEnableRescueOpts specifies options for enabling rescue mode for a server. type ServerEnableRescueOpts struct { Type ServerRescueType SSHKeys []*SSHKey } // ServerEnableRescueResult is the result of enabling rescue mode for a server. type ServerEnableRescueResult struct { Action *Action RootPassword string } // EnableRescue enables rescue mode for a server. func (c *ServerClient) EnableRescue(ctx context.Context, server *Server, opts ServerEnableRescueOpts) (ServerEnableRescueResult, *Response, error) { reqBody := schema.ServerActionEnableRescueRequest{ Type: String(string(opts.Type)), } for _, sshKey := range opts.SSHKeys { reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID) } reqBodyData, err := json.Marshal(reqBody) if err != nil { return ServerEnableRescueResult{}, nil, err } path := fmt.Sprintf("/servers/%d/actions/enable_rescue", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return ServerEnableRescueResult{}, nil, err } respBody := schema.ServerActionEnableRescueResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return ServerEnableRescueResult{}, resp, err } result := ServerEnableRescueResult{ Action: ActionFromSchema(respBody.Action), RootPassword: respBody.RootPassword, } return result, resp, nil } // DisableRescue disables rescue mode for a server. func (c *ServerClient) DisableRescue(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/disable_rescue", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionDisableRescueResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // ServerRebuildOpts specifies options for rebuilding a server. type ServerRebuildOpts struct { Image *Image } // Rebuild rebuilds a server. func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerRebuildOpts) (*Action, *Response, error) { reqBody := schema.ServerActionRebuildRequest{} if opts.Image.ID != 0 { reqBody.Image = opts.Image.ID } else { reqBody.Image = opts.Image.Name } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/rebuild", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionRebuildResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // AttachISO attaches an ISO to a server. func (c *ServerClient) AttachISO(ctx context.Context, server *Server, iso *ISO) (*Action, *Response, error) { reqBody := schema.ServerActionAttachISORequest{} if iso.ID != 0 { reqBody.ISO = iso.ID } else { reqBody.ISO = iso.Name } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/attach_iso", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionAttachISOResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // DetachISO detaches the currently attached ISO from a server. func (c *ServerClient) DetachISO(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/detach_iso", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionDetachISOResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // EnableBackup enables backup for a server. Pass in an empty backup window to let the // API pick a window for you. See the API documentation at docs.hetzner.cloud for a list // of valid backup windows. func (c *ServerClient) EnableBackup(ctx context.Context, server *Server, window string) (*Action, *Response, error) { reqBody := schema.ServerActionEnableBackupRequest{} if window != "" { reqBody.BackupWindow = String(window) } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/enable_backup", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionEnableBackupResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // DisableBackup disables backup for a server. func (c *ServerClient) DisableBackup(ctx context.Context, server *Server) (*Action, *Response, error) { path := fmt.Sprintf("/servers/%d/actions/disable_backup", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, nil) if err != nil { return nil, nil, err } respBody := schema.ServerActionDisableBackupResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // ServerChangeTypeOpts specifies options for changing a server's type. type ServerChangeTypeOpts struct { ServerType *ServerType // new server type UpgradeDisk bool // whether disk should be upgraded } // ChangeType changes a server's type. func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts ServerChangeTypeOpts) (*Action, *Response, error) { reqBody := schema.ServerActionChangeTypeRequest{ UpgradeDisk: opts.UpgradeDisk, } if opts.ServerType.ID != 0 { reqBody.ServerType = opts.ServerType.ID } else { reqBody.ServerType = opts.ServerType.Name } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/change_type", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionChangeTypeResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // ChangeDNSPtr changes or resets the reverse DNS pointer for a server IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (c *ServerClient) ChangeDNSPtr(ctx context.Context, server *Server, ip string, ptr *string) (*Action, *Response, error) { reqBody := schema.ServerActionChangeDNSPtrRequest{ IP: ip, DNSPtr: ptr, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionChangeDNSPtrResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // ServerChangeProtectionOpts specifies options for changing the resource protection level of a server. type ServerChangeProtectionOpts struct { Rebuild *bool Delete *bool } // ChangeProtection changes the resource protection level of a server. func (c *ServerClient) ChangeProtection(ctx context.Context, server *Server, opts ServerChangeProtectionOpts) (*Action, *Response, error) { reqBody := schema.ServerActionChangeProtectionRequest{ Rebuild: opts.Rebuild, Delete: opts.Delete, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/change_protection", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionChangeProtectionResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } // ServerAttachToNetworkOpts specifies options for attaching a server to a network. type ServerAttachToNetworkOpts struct { Network *Network IP net.IP AliasIPs []net.IP } // AttachToNetwork attaches a server to a network. func (c *ServerClient) AttachToNetwork(ctx context.Context, server *Server, opts ServerAttachToNetworkOpts) (*Action, *Response, error) { reqBody := schema.ServerActionAttachToNetworkRequest{ Network: opts.Network.ID, } if opts.IP != nil { reqBody.IP = String(opts.IP.String()) } for _, aliasIP := range opts.AliasIPs { reqBody.AliasIPs = append(reqBody.AliasIPs, String(aliasIP.String())) } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/attach_to_network", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionAttachToNetworkResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } // ServerDetachFromNetworkOpts specifies options for detaching a server from a network. type ServerDetachFromNetworkOpts struct { Network *Network } // DetachFromNetwork detaches a server from a network. func (c *ServerClient) DetachFromNetwork(ctx context.Context, server *Server, opts ServerDetachFromNetworkOpts) (*Action, *Response, error) { reqBody := schema.ServerActionDetachFromNetworkRequest{ Network: opts.Network.ID, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/detach_from_network", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionDetachFromNetworkResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } // ServerChangeAliasIPsOpts specifies options for changing the alias ips of an already attached network. type ServerChangeAliasIPsOpts struct { Network *Network AliasIPs []net.IP } // ChangeAliasIPs changes a server's alias IPs in a network. func (c *ServerClient) ChangeAliasIPs(ctx context.Context, server *Server, opts ServerChangeAliasIPsOpts) (*Action, *Response, error) { reqBody := schema.ServerActionChangeAliasIPsRequest{ Network: opts.Network.ID, AliasIPs: []string{}, } for _, aliasIP := range opts.AliasIPs { reqBody.AliasIPs = append(reqBody.AliasIPs, aliasIP.String()) } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/servers/%d/actions/change_alias_ips", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.ServerActionDetachFromNetworkResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } hcloud-go-1.17.0/hcloud/server_test.go000066400000000000000000001227171356226054300176530ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestServerClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerGetResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } t.Run("called via Get", func(t *testing.T) { server, _, err := env.Client.Server.Get(ctx, "1") if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } }) } func TestServerClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if server != nil { t.Fatal("expected no server") } } func TestServerClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myserver" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerListResponse{ Servers: []schema.Server{ { ID: 1, Name: "myserver", }, }, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByName(ctx, "myserver") if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } t.Run("via Get", func(t *testing.T) { server, _, err := env.Client.Server.Get(ctx, "myserver") if err != nil { t.Fatal(err) } if server == nil { t.Fatal("no server") } if server.ID != 1 { t.Errorf("unexpected server ID: %v", server.ID) } }) } func TestServerClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=myserver" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerListResponse{ Servers: []schema.Server{}, }) }) ctx := context.Background() server, _, err := env.Client.Server.GetByName(ctx, "myserver") if err != nil { t.Fatal(err) } if server != nil { t.Fatal("unexpected server") } } func TestServersList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ServerListResponse{ Servers: []schema.Server{ {ID: 1}, {ID: 2}, }, }) }) opts := ServerListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() servers, _, err := env.Client.Server.List(ctx, opts) if err != nil { t.Fatal(err) } if len(servers) != 2 { t.Fatal("expected 2 servers") } } func TestServersAll(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Servers []schema.Server `json:"servers"` Meta schema.Meta `json:"meta"` }{ Servers: []schema.Server{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() servers, err := env.Client.Server.All(ctx) if err != nil { t.Fatalf("Servers.List failed: %s", err) } if len(servers) != 3 { t.Fatalf("expected 3 servers; got %d", len(servers)) } if servers[0].ID != 1 || servers[1].ID != 2 || servers[2].ID != 3 { t.Errorf("unexpected servers") } } func TestServersAllWithOpts(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } if name := r.URL.Query().Get("name"); name != "my-server" { t.Errorf("unexpected name: %s", name) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { Servers []schema.Server `json:"servers"` Meta schema.Meta `json:"meta"` }{ Servers: []schema.Server{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() opts := ServerListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}, Name: "my-server"} servers, err := env.Client.Server.AllWithOpts(ctx, opts) if err != nil { t.Fatalf("Servers.List failed: %s", err) } if len(servers) != 3 { t.Fatalf("expected 3 servers; got %d", len(servers)) } if servers[0].ID != 1 || servers[1].ID != 2 || servers[2].ID != 3 { t.Errorf("unexpected servers") } } func TestServersCreateWithSSHKeys(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.SSHKeys) != 2 || reqBody.SSHKeys[0] != 1 || reqBody.SSHKeys[1] != 2 { t.Errorf("unexpected SSH keys: %v", reqBody.SSHKeys) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, SSHKeys: []*SSHKey{ {ID: 1}, {ID: 2}, }, }) if err != nil { t.Fatalf("Server.Create failed: %s", err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if result.RootPassword != "" { t.Errorf("expected no root password, got: %v", result.RootPassword) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestServersCreateWithoutSSHKeys(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.SSHKeys) != 0 { t.Errorf("expected no SSH keys, but got %v", reqBody.SSHKeys) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, RootPassword: String("test"), }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, }) if err != nil { t.Fatalf("Server.Create failed: %s", err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if result.RootPassword != "test" { t.Errorf("unexpected root password: %v", result.RootPassword) } } func TestServersCreateWithVolumes(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.Volumes) != 2 || reqBody.Volumes[0] != 1 || reqBody.Volumes[1] != 2 { t.Errorf("unexpected Volumes: %v", reqBody.Volumes) } if reqBody.Automount == nil || !*reqBody.Automount { t.Errorf("unexpected Automount: %v", reqBody.Automount) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Volumes: []*Volume{ {ID: 1}, {ID: 2}, }, Automount: Bool(true), }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestServersCreateWithNetworks(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.Networks) != 2 || reqBody.Networks[0] != 1 || reqBody.Networks[1] != 2 { t.Errorf("unexpected Networks: %v", reqBody.Networks) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, NextActions: []schema.Action{ {ID: 2}, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Networks: []*Network{ {ID: 1}, {ID: 2}, }, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestServersCreateWithDatacenterID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Datacenter != "1" { t.Errorf("unexpected datacenter: %v", reqBody.Datacenter) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Datacenter: &Datacenter{ID: 1}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithDatacenterName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Datacenter != "dc1" { t.Errorf("unexpected datacenter: %v", reqBody.Datacenter) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Datacenter: &Datacenter{Name: "dc1"}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithLocationID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Location != "1" { t.Errorf("unexpected location: %v", reqBody.Location) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Location: &Location{ID: 1}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithLocationName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Location != "loc1" { t.Errorf("unexpected location: %v", reqBody.Location) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Location: &Location{Name: "loc1"}, }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithUserData(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.UserData != "---user data---" { t.Errorf("unexpected userdata: %v", reqBody.UserData) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, UserData: "---user data---", }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersCreateWithLabels(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if len(reqBody.SSHKeys) != 0 { t.Errorf("expected no SSH keys, but got %v", reqBody.SSHKeys) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, RootPassword: String("test"), }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, Labels: map[string]string{"key": "value"}, }) if err != nil { t.Fatalf("Server.Create failed: %s", err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } } func TestServersCreateWithoutStarting(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.StartAfterCreate == nil || *reqBody.StartAfterCreate { t.Errorf("unexpected value for start_after_create: %v", reqBody.StartAfterCreate) } json.NewEncoder(w).Encode(schema.ServerCreateResponse{ Server: schema.Server{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{ Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, StartAfterCreate: Bool(false), }) if err != nil { t.Fatal(err) } if result.Server == nil { t.Fatal("no server") } } func TestServersDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { return }) var ( ctx = context.Background() server = &Server{ID: 1} ) _, err := env.Client.Server.Delete(ctx, server) if err != nil { t.Fatalf("Server.Delete failed: %s", err) } } func TestServerClientUpdate(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.ServerUpdateResponse{ Server: schema.Server{ ID: 1, }, }) }) opts := ServerUpdateOpts{ Name: "test", } updatedServer, _, err := env.Client.Server.Update(ctx, server, opts) if err != nil { t.Fatal(err) } if updatedServer.ID != 1 { t.Errorf("unexpected server ID: %v", updatedServer.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.ServerUpdateResponse{ Server: schema.Server{ ID: 1, }, }) }) opts := ServerUpdateOpts{ Labels: map[string]string{"key": "value"}, } updatedServer, _, err := env.Client.Server.Update(ctx, server, opts) if err != nil { t.Fatal(err) } if updatedServer.ID != 1 { t.Errorf("unexpected server ID: %v", updatedServer.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.ServerUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.ServerUpdateResponse{ Server: schema.Server{ ID: 1, }, }) }) opts := ServerUpdateOpts{} updatedServer, _, err := env.Client.Server.Update(ctx, server, opts) if err != nil { t.Fatal(err) } if updatedServer.ID != 1 { t.Errorf("unexpected server ID: %v", updatedServer.ID) } }) } func TestServerClientPoweron(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/poweron", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionPoweronResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Poweron(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientReboot(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/reboot", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionRebootResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Reboot(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientReset(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/reset", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionResetResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Reset(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientShutdown(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/shutdown", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionShutdownResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Shutdown(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientPoweroff(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/poweroff", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionPoweroffResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.Poweroff(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientResetPassword(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/reset_password", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionResetPasswordResponse{ Action: schema.Action{ ID: 1, }, RootPassword: "secret", }) }) ctx := context.Background() result, _, err := env.Client.Server.ResetPassword(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.RootPassword != "secret" { t.Errorf("unexpected root password: %v", result.RootPassword) } } func TestServerClientCreateImageNoOptions(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/create_image", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionCreateImageResponse{ Action: schema.Action{ ID: 1, }, Image: schema.Image{ ID: 1, }, }) }) ctx := context.Background() result, _, err := env.Client.Server.CreateImage(ctx, &Server{ID: 1}, nil) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.Image.ID != 1 { t.Errorf("unexpected image ID: %d", result.Image.ID) } } func TestServerClientCreateImageWithOptions(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/create_image", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionCreateImageRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type == nil || *reqBody.Type != "backup" { t.Errorf("unexpected type: %v", reqBody.Type) } if reqBody.Description == nil || *reqBody.Description != "my backup" { t.Errorf("unexpected description: %v", reqBody.Description) } json.NewEncoder(w).Encode(schema.ServerActionCreateImageResponse{ Action: schema.Action{ ID: 1, }, Image: schema.Image{ ID: 1, }, }) }) ctx := context.Background() opts := &ServerCreateImageOpts{ Type: ImageTypeBackup, Description: String("my backup"), } result, _, err := env.Client.Server.CreateImage(ctx, &Server{ID: 1}, opts) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.Image.ID != 1 { t.Errorf("unexpected image ID: %d", result.Image.ID) } } func TestServerClientEnableRescue(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/enable_rescue", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionEnableRescueRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Type == nil || *reqBody.Type != "linux64" { t.Errorf("unexpected type: %v", reqBody.Type) } if len(reqBody.SSHKeys) != 2 || reqBody.SSHKeys[0] != 1 || reqBody.SSHKeys[1] != 2 { t.Errorf("unexpected SSH keys: %v", reqBody.SSHKeys) } json.NewEncoder(w).Encode(schema.ServerActionEnableRescueResponse{ Action: schema.Action{ ID: 1, }, RootPassword: "test", }) }) ctx := context.Background() opts := ServerEnableRescueOpts{ Type: ServerRescueTypeLinux64, SSHKeys: []*SSHKey{ {ID: 1}, {ID: 2}, }, } result, _, err := env.Client.Server.EnableRescue(ctx, &Server{ID: 1}, opts) if err != nil { t.Fatal(err) } if result.Action.ID != 1 { t.Errorf("unexpected action ID: %d", result.Action.ID) } if result.RootPassword != "test" { t.Errorf("unexpected root password: %s", result.RootPassword) } } func TestServerClientDisableRescue(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/disable_rescue", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionDisableRescueResponse{ Action: schema.Action{ ID: 1, }, }) }) ctx := context.Background() action, _, err := env.Client.Server.DisableRescue(ctx, &Server{ID: 1}) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientRebuild(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with image ID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/rebuild", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionRebuildRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if id, ok := reqBody.Image.(float64); !ok || id != 1 { t.Errorf("unexpected image ID: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerRebuildOpts{ Image: &Image{ID: 1}, } action, _, err := env.Client.Server.Rebuild(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("with image name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/rebuild", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionRebuildRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if name, ok := reqBody.Image.(string); !ok || name != "debian-9" { t.Errorf("unexpected image name: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerRebuildOpts{ Image: &Image{Name: "debian-9"}, } action, _, err := env.Client.Server.Rebuild(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientAttachISO(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with ISO ID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_iso", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionAttachISORequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if id, ok := reqBody.ISO.(float64); !ok || id != 1 { t.Errorf("unexpected ISO ID: %v", reqBody.ISO) } json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{ Action: schema.Action{ ID: 1, }, }) }) iso := &ISO{ID: 1} action, _, err := env.Client.Server.AttachISO(ctx, server, iso) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("with ISO name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_iso", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionAttachISORequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if name, ok := reqBody.ISO.(string); !ok || name != "debian.iso" { t.Errorf("unexpected ISO name: %v", reqBody.ISO) } json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{ Action: schema.Action{ ID: 1, }, }) }) iso := &ISO{Name: "debian.iso"} action, _, err := env.Client.Server.AttachISO(ctx, server, iso) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientDetachISO(t *testing.T) { env := newTestEnv() defer env.Teardown() var ( ctx = context.Background() server = &Server{ID: 1} ) env.Mux.HandleFunc("/servers/1/actions/detach_iso", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionDetachISOResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.DetachISO(ctx, server) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientEnableBackup(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with a backup window", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/enable_backup", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionEnableBackupRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.BackupWindow == nil || *reqBody.BackupWindow != "9-17" { t.Errorf("unexpected backup window: %v", reqBody.BackupWindow) } json.NewEncoder(w).Encode(schema.ServerActionEnableBackupResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.EnableBackup(ctx, server, "9-17") if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("without a backup window", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/enable_backup", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionEnableBackupRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.BackupWindow != nil { t.Errorf("unexpected backup window: %v", reqBody.BackupWindow) } json.NewEncoder(w).Encode(schema.ServerActionEnableBackupResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.EnableBackup(ctx, server, "") if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientDisableBackup(t *testing.T) { env := newTestEnv() defer env.Teardown() var ( ctx = context.Background() server = &Server{ID: 1} ) env.Mux.HandleFunc("/servers/1/actions/disable_backup", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerActionDisableBackupResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.DisableBackup(ctx, server) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } } func TestServerClientChangeType(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("with server type ID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_type", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionChangeTypeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if id, ok := reqBody.ServerType.(float64); !ok || id != 1 { t.Errorf("unexpected server type ID: %v", reqBody.ServerType) } if !reqBody.UpgradeDisk { t.Error("expected to upgrade disk") } json.NewEncoder(w).Encode(schema.ServerActionChangeTypeResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerChangeTypeOpts{ ServerType: &ServerType{ID: 1}, UpgradeDisk: true, } action, _, err := env.Client.Server.ChangeType(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("with server type name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_type", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionChangeTypeRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if name, ok := reqBody.ServerType.(string); !ok || name != "type" { t.Errorf("unexpected server type name: %v", reqBody.ServerType) } if !reqBody.UpgradeDisk { t.Error("expected to upgrade disk") } json.NewEncoder(w).Encode(schema.ServerActionChangeTypeResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerChangeTypeOpts{ ServerType: &ServerType{Name: "type"}, UpgradeDisk: true, } action, _, err := env.Client.Server.ChangeType(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientChangeDNSPtr(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("set", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionChangeDNSPtrRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.IP != "127.0.0.1" { t.Errorf("unexpected IP: %v", reqBody.IP) } if reqBody.DNSPtr == nil || *reqBody.DNSPtr != "example.com" { t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr) } json.NewEncoder(w).Encode(schema.ServerActionChangeDNSPtrResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.ChangeDNSPtr(ctx, server, "127.0.0.1", String("example.com")) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) t.Run("reset", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.ServerActionChangeDNSPtrRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.IP != "127.0.0.1" { t.Errorf("unexpected IP: %v", reqBody.IP) } if reqBody.DNSPtr != nil { t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr) } json.NewEncoder(w).Encode(schema.ServerActionChangeDNSPtrResponse{ Action: schema.Action{ ID: 1, }, }) }) action, _, err := env.Client.Server.ChangeDNSPtr(ctx, server, "127.0.0.1", nil) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %d", action.ID) } }) } func TestServerClientChangeProtection(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("enable delete and rebuild protection", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_protection", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionChangeProtectionRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Delete == nil || *reqBody.Delete != true { t.Errorf("unexpected delete: %v", reqBody.Delete) } if reqBody.Rebuild == nil || *reqBody.Rebuild != true { t.Errorf("unexpected rebuild: %v", reqBody.Rebuild) } json.NewEncoder(w).Encode(schema.ServerActionChangeProtectionResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerChangeProtectionOpts{ Delete: Bool(true), Rebuild: Bool(true), } action, _, err := env.Client.Server.ChangeProtection(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } func TestServerClientAttachToNetwork(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) t.Run("attach to network", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_to_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionAttachToNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerAttachToNetworkOpts{ Network: &Network{ID: 1}, } action, _, err := env.Client.Server.AttachToNetwork(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) t.Run("attach to network with additional parameters", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/attach_to_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionAttachToNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } if reqBody.IP == nil || *reqBody.IP != "10.0.1.1" { t.Errorf("unexpected IP: %v", *reqBody.IP) } if len(reqBody.AliasIPs) == 0 || *reqBody.AliasIPs[0] != "10.0.1.1" { t.Errorf("unexpected AliasIPs: %v", *reqBody.IP) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) ip := net.ParseIP("10.0.1.1") aliasIPs := []net.IP{ ip, } opts := ServerAttachToNetworkOpts{ Network: &Network{ID: 1}, IP: ip, AliasIPs: aliasIPs, } action, _, err := env.Client.Server.AttachToNetwork(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } }) } func TestServerClientDetachFromNetwork(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/detach_from_network", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionDetachFromNetworkRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) opts := ServerDetachFromNetworkOpts{ Network: &Network{ID: 1}, } action, _, err := env.Client.Server.DetachFromNetwork(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } func TestServerClientChangeAliasIP(t *testing.T) { var ( ctx = context.Background() server = &Server{ID: 1} ) env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/servers/1/actions/change_alias_ips", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("expected POST") } var reqBody schema.ServerActionChangeAliasIPsRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Network != 1 { t.Errorf("unexpected Network: %v", reqBody.Network) } if len(reqBody.AliasIPs) == 0 || reqBody.AliasIPs[0] != "10.0.1.1" { t.Errorf("unexpected AliasIPs: %v", reqBody.AliasIPs[0]) } json.NewEncoder(w).Encode(schema.ServerActionAttachToNetworkResponse{ Action: schema.Action{ ID: 1, }, }) }) ip := net.ParseIP("10.0.1.1") aliasIPs := []net.IP{ ip, } opts := ServerChangeAliasIPsOpts{ Network: &Network{ID: 1}, AliasIPs: aliasIPs, } action, _, err := env.Client.Server.ChangeAliasIPs(ctx, server, opts) if err != nil { t.Fatal(err) } if action.ID != 1 { t.Errorf("unexpected action ID: %v", action.ID) } } hcloud-go-1.17.0/hcloud/server_type.go000066400000000000000000000073631356226054300176540ustar00rootroot00000000000000package hcloud import ( "context" "fmt" "net/url" "strconv" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // ServerType represents a server type in the Hetzner Cloud. type ServerType struct { ID int Name string Description string Cores int Memory float32 Disk int StorageType StorageType CPUType CPUType Pricings []ServerTypeLocationPricing } // StorageType specifies the type of storage. type StorageType string const ( // StorageTypeLocal is the type for local storage. StorageTypeLocal StorageType = "local" // StorageTypeCeph is the type for remote storage. StorageTypeCeph StorageType = "ceph" ) // CPUType specifies the type of the CPU. type CPUType string const ( // CPUTypeShared is the type for shared CPU. CPUTypeShared CPUType = "shared" //CPUTypeDedicated is the type for dedicated CPU. CPUTypeDedicated CPUType = "dedicated" ) // ServerTypeClient is a client for the server types API. type ServerTypeClient struct { client *Client } // GetByID retrieves a server type by its ID. If the server type does not exist, nil is returned. func (c *ServerTypeClient) GetByID(ctx context.Context, id int) (*ServerType, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/server_types/%d", id), nil) if err != nil { return nil, nil, err } var body schema.ServerTypeGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return ServerTypeFromSchema(body.ServerType), resp, nil } // GetByName retrieves a server type by its name. If the server type does not exist, nil is returned. func (c *ServerTypeClient) GetByName(ctx context.Context, name string) (*ServerType, *Response, error) { serverTypes, response, err := c.List(ctx, ServerTypeListOpts{Name: name}) if len(serverTypes) == 0 { return nil, response, err } return serverTypes[0], response, err } // Get retrieves a server type by its ID if the input can be parsed as an integer, otherwise it // retrieves a server type by its name. If the server type does not exist, nil is returned. func (c *ServerTypeClient) Get(ctx context.Context, idOrName string) (*ServerType, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // ServerTypeListOpts specifies options for listing server types. type ServerTypeListOpts struct { ListOpts Name string } func (l ServerTypeListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } return vals } // List returns a list of server types for a specific page. func (c *ServerTypeClient) List(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, *Response, error) { path := "/server_types?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.ServerTypeListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } serverTypes := make([]*ServerType, 0, len(body.ServerTypes)) for _, s := range body.ServerTypes { serverTypes = append(serverTypes, ServerTypeFromSchema(s)) } return serverTypes, resp, nil } // All returns all server types. func (c *ServerTypeClient) All(ctx context.Context) ([]*ServerType, error) { allServerTypes := []*ServerType{} opts := ServerTypeListOpts{} opts.PerPage = 50 _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page serverTypes, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allServerTypes = append(allServerTypes, serverTypes...) return resp, nil }) if err != nil { return nil, err } return allServerTypes, nil } hcloud-go-1.17.0/hcloud/server_type_test.go000066400000000000000000000117251356226054300207100ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestServerTypeClient(t *testing.T) { t.Run("GetByID", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types/1", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(schema.ServerTypeGetResponse{ ServerType: schema.ServerType{ ID: 1, }, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no server type") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } t.Run("via Get", func(t *testing.T) { serverType, _, err := env.Client.ServerType.Get(ctx, "1") if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no server type") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } }) }) t.Run("GetByID (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if serverType != nil { t.Fatal("expected no server type") } }) t.Run("GetByName", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=cx10" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerTypeListResponse{ ServerTypes: []schema.ServerType{ { ID: 1, }, }, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByName(ctx, "cx10") if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no server type") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } t.Run("via Get", func(t *testing.T) { serverType, _, err := env.Client.ServerType.Get(ctx, "cx10") if err != nil { t.Fatal(err) } if serverType == nil { t.Fatal("no serverType") } if serverType.ID != 1 { t.Errorf("unexpected server type ID: %v", serverType.ID) } }) }) t.Run("GetByName (not found)", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=cx10" { t.Fatal("missing name query") } json.NewEncoder(w).Encode(schema.ServerTypeListResponse{ ServerTypes: []schema.ServerType{}, }) }) ctx := context.Background() serverType, _, err := env.Client.ServerType.GetByName(ctx, "cx10") if err != nil { t.Fatal(err) } if serverType != nil { t.Fatal("unexpected server type") } }) t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } json.NewEncoder(w).Encode(schema.ServerTypeListResponse{ ServerTypes: []schema.ServerType{ {ID: 1}, {ID: 2}, }, }) }) opts := ServerTypeListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() serverTypes, _, err := env.Client.ServerType.List(ctx, opts) if err != nil { t.Fatal(err) } if len(serverTypes) != 2 { t.Fatal("expected 2 server types") } }) t.Run("All", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/server_types", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { ServerTypes []schema.ServerType `json:"server_types"` Meta schema.Meta `json:"meta"` }{ ServerTypes: []schema.ServerType{ {ID: 1}, {ID: 2}, {ID: 3}, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 3, PerPage: 3, TotalEntries: 3, }, }, }) }) ctx := context.Background() serverTypes, err := env.Client.ServerType.All(ctx) if err != nil { t.Fatalf("ServerTypes.List failed: %s", err) } if len(serverTypes) != 3 { t.Fatalf("expected 3 server types; got %d", len(serverTypes)) } if serverTypes[0].ID != 1 || serverTypes[1].ID != 2 || serverTypes[2].ID != 3 { t.Errorf("unexpected server types") } }) } hcloud-go-1.17.0/hcloud/ssh_key.go000066400000000000000000000136551356226054300167530ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // SSHKey represents a SSH key in the Hetzner Cloud. type SSHKey struct { ID int Name string Fingerprint string PublicKey string Labels map[string]string Created time.Time } // SSHKeyClient is a client for the SSH keys API. type SSHKeyClient struct { client *Client } // GetByID retrieves a SSH key by its ID. If the SSH key does not exist, nil is returned. func (c *SSHKeyClient) GetByID(ctx context.Context, id int) (*SSHKey, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/ssh_keys/%d", id), nil) if err != nil { return nil, nil, err } var body schema.SSHKeyGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return SSHKeyFromSchema(body.SSHKey), resp, nil } // GetByName retrieves a SSH key by its name. If the SSH key does not exist, nil is returned. func (c *SSHKeyClient) GetByName(ctx context.Context, name string) (*SSHKey, *Response, error) { sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Name: name}) if len(sshKeys) == 0 { return nil, response, err } return sshKeys[0], response, err } // GetByFingerprint retreives a SSH key by its fingerprint. If the SSH key does not exist, nil is returned. func (c *SSHKeyClient) GetByFingerprint(ctx context.Context, fingerprint string) (*SSHKey, *Response, error) { sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Fingerprint: fingerprint}) if len(sshKeys) == 0 { return nil, response, err } return sshKeys[0], response, err } // Get retrieves a SSH key by its ID if the input can be parsed as an integer, otherwise it // retrieves a SSH key by its name. If the SSH key does not exist, nil is returned. func (c *SSHKeyClient) Get(ctx context.Context, idOrName string) (*SSHKey, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // SSHKeyListOpts specifies options for listing SSH keys. type SSHKeyListOpts struct { ListOpts Name string Fingerprint string } func (l SSHKeyListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } if l.Fingerprint != "" { vals.Add("fingerprint", l.Fingerprint) } return vals } // List returns a list of SSH keys for a specific page. func (c *SSHKeyClient) List(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, *Response, error) { path := "/ssh_keys?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.SSHKeyListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } sshKeys := make([]*SSHKey, 0, len(body.SSHKeys)) for _, s := range body.SSHKeys { sshKeys = append(sshKeys, SSHKeyFromSchema(s)) } return sshKeys, resp, nil } // All returns all SSH keys. func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) { return c.AllWithOpts(ctx, SSHKeyListOpts{ListOpts: ListOpts{PerPage: 50}}) } // AllWithOpts returns all SSH keys with the given options. func (c *SSHKeyClient) AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) { allSSHKeys := []*SSHKey{} _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page sshKeys, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allSSHKeys = append(allSSHKeys, sshKeys...) return resp, nil }) if err != nil { return nil, err } return allSSHKeys, nil } // SSHKeyCreateOpts specifies parameters for creating a SSH key. type SSHKeyCreateOpts struct { Name string PublicKey string Labels map[string]string } // Validate checks if options are valid. func (o SSHKeyCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } if o.PublicKey == "" { return errors.New("missing public key") } return nil } // Create creates a new SSH key with the given options. func (c *SSHKeyClient) Create(ctx context.Context, opts SSHKeyCreateOpts) (*SSHKey, *Response, error) { if err := opts.Validate(); err != nil { return nil, nil, err } reqBody := schema.SSHKeyCreateRequest{ Name: opts.Name, PublicKey: opts.PublicKey, } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/ssh_keys", bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.SSHKeyCreateResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return SSHKeyFromSchema(respBody.SSHKey), resp, nil } // Delete deletes a SSH key. func (c *SSHKeyClient) Delete(ctx context.Context, sshKey *SSHKey) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/ssh_keys/%d", sshKey.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } // SSHKeyUpdateOpts specifies options for updating a SSH key. type SSHKeyUpdateOpts struct { Name string Labels map[string]string } // Update updates a SSH key. func (c *SSHKeyClient) Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUpdateOpts) (*SSHKey, *Response, error) { reqBody := schema.SSHKeyUpdateRequest{ Name: opts.Name, } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/ssh_keys/%d", sshKey.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.SSHKeyUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return SSHKeyFromSchema(respBody.SSHKey), resp, nil } hcloud-go-1.17.0/hcloud/ssh_key_test.go000066400000000000000000000322021356226054300177770ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestSSHKeyClientGetByID(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "ssh_key": { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "created": "2017-08-16T17:29:14+00:00" } }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByID(ctx, 1) if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } t.Run("via Get", func(t *testing.T) { sshKey, _, err := env.Client.SSHKey.Get(ctx, "1") if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } }) } func TestSSHKeyClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByID(ctx, 1) if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey != nil { t.Fatal("expected no SSH key") } } func TestSSHKeyClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=My+Key" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "ssh_keys": [{ "id": 1, "name": "My Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt" }] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByName(ctx, "My Key") if err != nil { t.Fatalf("SSHKey.GetByName failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } t.Run("via Get", func(t *testing.T) { sshKey, _, err := env.Client.SSHKey.Get(ctx, "My Key") if err != nil { t.Fatalf("SSHKey.GetByID failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } }) } func TestSSHKeyClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=My+Key" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "ssh_keys": [] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByName(ctx, "My Key") if err != nil { t.Fatalf("SSHKey.GetByName failed: %s", err) } if sshKey != nil { t.Fatal("unexpected SSH key") } } func TestSSHKeyClientGetByFingerprint(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("fingerprint") != "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0" { t.Fatal("missing or invalid fingerprint query") } fmt.Fprint(w, `{ "ssh_keys": [{ "id": 1, "name": "My Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "created": "2017-08-16T17:29:14+00:00" }] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByFingerprint(ctx, "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0") if err != nil { t.Fatalf("SSHKey.GetByFingerprint failed: %s", err) } if sshKey == nil { t.Fatal("no SSH key") } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } } func TestSSHKeyClientGetByFingerprintNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("fingerprint") != "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0" { t.Fatal("missing or invalid fingerprint query") } fmt.Fprint(w, `{ "ssh_keys": [] }`) }) ctx := context.Background() sshKey, _, err := env.Client.SSHKey.GetByFingerprint(ctx, "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0") if err != nil { t.Fatalf("SSHKey.GetByFingerprint failed: %s", err) } if sshKey != nil { t.Fatal("unexpected SSH key") } } func TestSSHKeyClientList(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if page := r.URL.Query().Get("page"); page != "2" { t.Errorf("expected page 2; got %q", page) } if perPage := r.URL.Query().Get("per_page"); perPage != "50" { t.Errorf("expected per_page 50; got %q", perPage) } fmt.Fprint(w, `{ "ssh_keys": [ { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "created": "2017-08-16T17:29:14+00:00" }, { "id": 2, "name": "Another key", "fingerprint": "c7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...XX", "created": "2017-08-16T17:29:14+00:00" } ] }`) }) opts := SSHKeyListOpts{} opts.Page = 2 opts.PerPage = 50 ctx := context.Background() sshKeys, _, err := env.Client.SSHKey.List(ctx, opts) if err != nil { t.Fatalf("SSHKey.List failed: %s", err) } if len(sshKeys) != 2 { t.Fatal("unexpected number of SSH keys") } if sshKeys[0].ID != 1 || sshKeys[1].ID != 2 { t.Fatalf("unexpected SSH key IDs: %d, %d", sshKeys[0].ID, sshKeys[1].ID) } } func TestSSHKeyAll(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { SSHKeys []schema.SSHKey `json:"ssh_keys"` Meta schema.Meta `json:"meta"` }{ SSHKeys: []schema.SSHKey{ { ID: 1, Name: "My key", Fingerprint: "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", }, { ID: 2, Name: "Another key", Fingerprint: "c7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...XX", }, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 2, TotalEntries: 2, }, }, }) }) ctx := context.Background() sshKeys, err := env.Client.SSHKey.All(ctx) if err != nil { t.Fatal(err) } if len(sshKeys) != 2 { t.Fatalf("expected 2 SSH keys; got %d", len(sshKeys)) } if sshKeys[0].ID != 1 || sshKeys[1].ID != 2 { t.Error("got wrong SSH keys") } } func TestSSHKeyAllWithOpts(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" { t.Errorf("unexpected label selector: %s", labelSelector) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(struct { SSHKeys []schema.SSHKey `json:"ssh_keys"` Meta schema.Meta `json:"meta"` }{ SSHKeys: []schema.SSHKey{ { ID: 1, Name: "My key", Fingerprint: "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", }, { ID: 2, Name: "Another key", Fingerprint: "c7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", PublicKey: "ssh-rsa AAAjjk76kgf...XX", }, }, Meta: schema.Meta{ Pagination: &schema.MetaPagination{ Page: 1, LastPage: 1, PerPage: 2, TotalEntries: 2, }, }, }) }) ctx := context.Background() opts := SSHKeyListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}} sshKeys, err := env.Client.SSHKey.AllWithOpts(ctx, opts) if err != nil { t.Fatal(err) } if len(sshKeys) != 2 { t.Fatalf("expected 2 SSH keys; got %d", len(sshKeys)) } if sshKeys[0].ID != 1 || sshKeys[1].ID != 2 { t.Error("got wrong SSH keys") } } func TestSSHKeyClientDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) {}) var ( ctx = context.Background() sshKey = &SSHKey{ID: 1} ) _, err := env.Client.SSHKey.Delete(ctx, sshKey) if err != nil { t.Fatalf("SSHKey.Delete failed: %s", err) } } func TestSSHKeyClientCreate(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "ssh_key": { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt" } }`) }) ctx := context.Background() opts := SSHKeyCreateOpts{ Name: "My key", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", } sshKey, _, err := env.Client.SSHKey.Create(ctx, opts) if err != nil { t.Fatalf("SSHKey.Get failed: %s", err) } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } } func TestSSHKeyClientCreateWithLabels(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.SSHKeyCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } fmt.Fprint(w, `{ "ssh_key": { "id": 1, "name": "My key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c", "public_key": "ssh-rsa AAAjjk76kgf...Xt" } }`) }) ctx := context.Background() opts := SSHKeyCreateOpts{ Name: "My key", PublicKey: "ssh-rsa AAAjjk76kgf...Xt", Labels: map[string]string{"key": "value"}, } sshKey, _, err := env.Client.SSHKey.Create(ctx, opts) if err != nil { t.Fatalf("SSHKey.Get failed: %s", err) } if sshKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", sshKey.ID) } } func TestSSHKeyClientUpdate(t *testing.T) { var ( ctx = context.Background() sshKey = &SSHKey{ID: 1} ) t.Run("update name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.SSHKeyUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{ SSHKey: schema.SSHKey{ ID: 1, }, }) }) opts := SSHKeyUpdateOpts{ Name: "test", } updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts) if err != nil { t.Fatal(err) } if updatedSSHKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID) } }) t.Run("update labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.SSHKeyUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{ SSHKey: schema.SSHKey{ ID: 1, }, }) }) opts := SSHKeyUpdateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, } updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts) if err != nil { t.Fatal(err) } if updatedSSHKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.SSHKeyUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{ SSHKey: schema.SSHKey{ ID: 1, }, }) }) opts := SSHKeyUpdateOpts{} updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts) if err != nil { t.Fatal(err) } if updatedSSHKey.ID != 1 { t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID) } }) } hcloud-go-1.17.0/hcloud/volume.go000066400000000000000000000247571356226054300166220ustar00rootroot00000000000000package hcloud import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/url" "strconv" "time" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) // Volume represents a volume in the Hetzner Cloud. type Volume struct { ID int Name string Server *Server Location *Location Size int Protection VolumeProtection Labels map[string]string LinuxDevice string Created time.Time } // VolumeProtection represents the protection level of a volume. type VolumeProtection struct { Delete bool } // VolumeClient is a client for the volume API. type VolumeClient struct { client *Client } // VolumeStatus specifies a volume's status. type VolumeStatus string const ( // VolumeStatusCreating is the status when a volume is being created. VolumeStatusCreating VolumeStatus = "creating" // VolumeStatusAvailable is the status when a volume is available. VolumeStatusAvailable VolumeStatus = "available" ) // GetByID retrieves a volume by its ID. If the volume does not exist, nil is returned. func (c *VolumeClient) GetByID(ctx context.Context, id int) (*Volume, *Response, error) { req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/volumes/%d", id), nil) if err != nil { return nil, nil, err } var body schema.VolumeGetResponse resp, err := c.client.Do(req, &body) if err != nil { if IsError(err, ErrorCodeNotFound) { return nil, resp, nil } return nil, nil, err } return VolumeFromSchema(body.Volume), resp, nil } // GetByName retrieves a volume by its name. If the volume does not exist, nil is returned. func (c *VolumeClient) GetByName(ctx context.Context, name string) (*Volume, *Response, error) { volumes, response, err := c.List(ctx, VolumeListOpts{Name: name}) if len(volumes) == 0 { return nil, response, err } return volumes[0], response, err } // Get retrieves a volume by its ID if the input can be parsed as an integer, otherwise it // retrieves a volume by its name. If the volume does not exist, nil is returned. func (c *VolumeClient) Get(ctx context.Context, idOrName string) (*Volume, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { return c.GetByID(ctx, int(id)) } return c.GetByName(ctx, idOrName) } // VolumeListOpts specifies options for listing volumes. type VolumeListOpts struct { ListOpts Name string Status []VolumeStatus } func (l VolumeListOpts) values() url.Values { vals := l.ListOpts.values() if l.Name != "" { vals.Add("name", l.Name) } for _, status := range l.Status { vals.Add("status", string(status)) } return vals } // List returns a list of volumes for a specific page. func (c *VolumeClient) List(ctx context.Context, opts VolumeListOpts) ([]*Volume, *Response, error) { path := "/volumes?" + opts.values().Encode() req, err := c.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } var body schema.VolumeListResponse resp, err := c.client.Do(req, &body) if err != nil { return nil, nil, err } volumes := make([]*Volume, 0, len(body.Volumes)) for _, s := range body.Volumes { volumes = append(volumes, VolumeFromSchema(s)) } return volumes, resp, nil } // All returns all volumes. func (c *VolumeClient) All(ctx context.Context) ([]*Volume, error) { return c.AllWithOpts(ctx, VolumeListOpts{ListOpts: ListOpts{PerPage: 50}}) } // AllWithOpts returns all volumes with the given options. func (c *VolumeClient) AllWithOpts(ctx context.Context, opts VolumeListOpts) ([]*Volume, error) { allVolumes := []*Volume{} _, err := c.client.all(func(page int) (*Response, error) { opts.Page = page volumes, resp, err := c.List(ctx, opts) if err != nil { return resp, err } allVolumes = append(allVolumes, volumes...) return resp, nil }) if err != nil { return nil, err } return allVolumes, nil } // VolumeCreateOpts specifies parameters for creating a volume. type VolumeCreateOpts struct { Name string Size int Server *Server Location *Location Labels map[string]string Automount *bool Format *string } // Validate checks if options are valid. func (o VolumeCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") } if o.Size <= 0 { return errors.New("size must be greater than 0") } if o.Server == nil && o.Location == nil { return errors.New("one of server or location must be provided") } if o.Server != nil && o.Location != nil { return errors.New("only one of server or location must be provided") } if o.Server == nil && (o.Automount != nil && *o.Automount) { return errors.New("server must be provided when automount is true") } return nil } // VolumeCreateResult is the result of creating a volume. type VolumeCreateResult struct { Volume *Volume Action *Action NextActions []*Action } // Create creates a new volume with the given options. func (c *VolumeClient) Create(ctx context.Context, opts VolumeCreateOpts) (VolumeCreateResult, *Response, error) { if err := opts.Validate(); err != nil { return VolumeCreateResult{}, nil, err } reqBody := schema.VolumeCreateRequest{ Name: opts.Name, Size: opts.Size, Automount: opts.Automount, Format: opts.Format, } if opts.Labels != nil { reqBody.Labels = &opts.Labels } if opts.Server != nil { reqBody.Server = Int(opts.Server.ID) } if opts.Location != nil { if opts.Location.ID != 0 { reqBody.Location = opts.Location.ID } else { reqBody.Location = opts.Location.Name } } reqBodyData, err := json.Marshal(reqBody) if err != nil { return VolumeCreateResult{}, nil, err } req, err := c.client.NewRequest(ctx, "POST", "/volumes", bytes.NewReader(reqBodyData)) if err != nil { return VolumeCreateResult{}, nil, err } var respBody schema.VolumeCreateResponse resp, err := c.client.Do(req, &respBody) if err != nil { return VolumeCreateResult{}, resp, err } var action *Action if respBody.Action != nil { action = ActionFromSchema(*respBody.Action) } return VolumeCreateResult{ Volume: VolumeFromSchema(respBody.Volume), Action: action, NextActions: ActionsFromSchema(respBody.NextActions), }, resp, nil } // Delete deletes a volume. func (c *VolumeClient) Delete(ctx context.Context, volume *Volume) (*Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/volumes/%d", volume.ID), nil) if err != nil { return nil, err } return c.client.Do(req, nil) } // VolumeUpdateOpts specifies options for updating a volume. type VolumeUpdateOpts struct { Name string Labels map[string]string } // Update updates a volume. func (c *VolumeClient) Update(ctx context.Context, volume *Volume, opts VolumeUpdateOpts) (*Volume, *Response, error) { reqBody := schema.VolumeUpdateRequest{ Name: opts.Name, } if opts.Labels != nil { reqBody.Labels = &opts.Labels } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/volumes/%d", volume.ID) req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.VolumeUpdateResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return VolumeFromSchema(respBody.Volume), resp, nil } // VolumeAttachOpts specifies options for attaching a volume. type VolumeAttachOpts struct { Server *Server Automount *bool } // AttachWithOpts attaches a volume to a server. func (c *VolumeClient) AttachWithOpts(ctx context.Context, volume *Volume, opts VolumeAttachOpts) (*Action, *Response, error) { reqBody := schema.VolumeActionAttachVolumeRequest{ Server: opts.Server.ID, Automount: opts.Automount, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/volumes/%d/actions/attach", volume.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.VolumeActionAttachVolumeResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // Attach attaches a volume to a server. func (c *VolumeClient) Attach(ctx context.Context, volume *Volume, server *Server) (*Action, *Response, error) { return c.AttachWithOpts(ctx, volume, VolumeAttachOpts{Server: server}) } // Detach detaches a volume from a server. func (c *VolumeClient) Detach(ctx context.Context, volume *Volume) (*Action, *Response, error) { var reqBody schema.VolumeActionDetachVolumeRequest reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/volumes/%d/actions/detach", volume.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } var respBody schema.VolumeActionDetachVolumeResponse resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, nil } // VolumeChangeProtectionOpts specifies options for changing the resource protection level of a volume. type VolumeChangeProtectionOpts struct { Delete *bool } // ChangeProtection changes the resource protection level of a volume. func (c *VolumeClient) ChangeProtection(ctx context.Context, volume *Volume, opts VolumeChangeProtectionOpts) (*Action, *Response, error) { reqBody := schema.VolumeActionChangeProtectionRequest{ Delete: opts.Delete, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/volumes/%d/actions/change_protection", volume.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.VolumeActionChangeProtectionResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } // Resize changes the size of a volume. func (c *VolumeClient) Resize(ctx context.Context, volume *Volume, size int) (*Action, *Response, error) { reqBody := schema.VolumeActionResizeVolumeRequest{ Size: size, } reqBodyData, err := json.Marshal(reqBody) if err != nil { return nil, nil, err } path := fmt.Sprintf("/volumes/%d/actions/resize", volume.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { return nil, nil, err } respBody := schema.VolumeActionResizeVolumeResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } return ActionFromSchema(respBody.Action), resp, err } hcloud-go-1.17.0/hcloud/volume_test.go000066400000000000000000000304241356226054300176450ustar00rootroot00000000000000package hcloud import ( "context" "encoding/json" "fmt" "net/http" "testing" "github.com/hetznercloud/hcloud-go/hcloud/schema" ) func TestVolumeClientGet(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "db-storage", "server": null, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true } } }`) }) ctx := context.Background() t.Run("GetByID", func(t *testing.T) { volume, _, err := env.Client.Volume.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) t.Run("Get", func(t *testing.T) { volume, _, err := env.Client.Volume.Get(ctx, "1") if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) } func TestVolumeClientGetByIDNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(schema.ErrorResponse{ Error: schema.Error{ Code: string(ErrorCodeNotFound), }, }) }) ctx := context.Background() volume, _, err := env.Client.Volume.GetByID(ctx, 1) if err != nil { t.Fatal(err) } if volume != nil { t.Fatal("expected no volume") } } func TestVolumeClientGetByName(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my-volume" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "volumes": [ { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "server": null, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true } } ] }`) }) ctx := context.Background() t.Run("GetByName", func(t *testing.T) { volume, _, err := env.Client.Volume.GetByName(ctx, "my-volume") if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) t.Run("Get", func(t *testing.T) { volume, _, err := env.Client.Volume.Get(ctx, "my-volume") if err != nil { t.Fatal(err) } if volume == nil { t.Fatal("no volume") } if volume.ID != 1 { t.Errorf("unexpected volume ID: %v", volume.ID) } }) } func TestVolumeClientGetByNameNotFound(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { if r.URL.RawQuery != "name=my-volume" { t.Fatal("missing name query") } fmt.Fprint(w, `{ "volumes": [] }`) }) ctx := context.Background() volume, _, err := env.Client.Volume.GetByName(ctx, "my-volume") if err != nil { t.Fatal(err) } if volume != nil { t.Fatal("unexpected volume") } } func TestVolumeClientDelete(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "DELETE" { t.Error("expected DELETE") } }) var ( ctx = context.Background() volume = &Volume{ID: 1} ) _, err := env.Client.Volume.Delete(ctx, volume) if err != nil { t.Fatal(err) } } func TestVolumeClientCreateWithServer(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "server": 1, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": {} }, "action": { "id": 2, "command": "create_volume", "status": "running", "progress": 0, "started": "2016-01-30T23:50:11+00:00", "finished": null, "resources": [ { "id": 42, "type": "server" }, { "id": 1, "type": "volume" } ] }, "next_actions": [ { "id": 3, "command": "attach_volume", "status": "running", "progress": 0, "started": "2016-01-30T23:50:15+00:00", "finished": null, "resources": [ { "id": 42, "type": "server" }, { "id": 1, "type": "volume" } ] } ] }`) }) ctx := context.Background() opts := VolumeCreateOpts{ Name: "my-volume", Size: 42, Server: &Server{ID: 1}, } result, _, err := env.Client.Volume.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.Volume.ID != 1 { t.Errorf("unexpected volume ID: %v", result.Volume.ID) } if result.Action.ID != 2 { t.Errorf("unexpected action ID: %v", result.Action.ID) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 3 { t.Errorf("unexpected next actions: %v", result.NextActions) } } func TestVolumeClientCreateWithLocation(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.VolumeCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "my-volume" { t.Errorf("unexpected volume name in request: %v", reqBody.Name) } if reqBody.Size != 42 { t.Errorf("unexpected volume size in request: %v", reqBody.Size) } if reqBody.Location != float64(1) { t.Errorf("unexpected volume location in request: %v", reqBody.Location) } if reqBody.Server != nil { t.Errorf("unexpected server in request: %v", reqBody.Server) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } if reqBody.Automount != nil { t.Errorf("unexpected automount in request: %v", reqBody.Automount) } if reqBody.Format != nil { t.Errorf("unexpected format in request: %v", reqBody.Automount) } fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "server": null, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": { "key": "value" } } }`) }) ctx := context.Background() opts := VolumeCreateOpts{ Name: "my-volume", Size: 42, Location: &Location{ID: 1}, Labels: map[string]string{"key": "value"}, } result, _, err := env.Client.Volume.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.Volume.ID != 1 { t.Errorf("unexpected volume ID: %v", result.Volume.ID) } } func TestVolumeClientCreateWithAutomount(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) { var reqBody schema.VolumeCreateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "my-volume" { t.Errorf("unexpected volume name in request: %v", reqBody.Name) } if reqBody.Size != 42 { t.Errorf("unexpected volume size in request: %v", reqBody.Size) } if reqBody.Server == nil || *reqBody.Server != 1 { t.Errorf("unexpected server in request: %v", reqBody.Server) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } if *reqBody.Automount != true { t.Errorf("unexpected automount in request: %v", reqBody.Automount) } if *reqBody.Format != "xfs" { t.Errorf("unexpected format in request: %v", reqBody.Automount) } fmt.Fprint(w, `{ "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "my-volume", "server": 1, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071 }, "size": 42, "linux_device":"/dev/disk/by-id/scsi-0HC_volume_1", "protection": { "delete": true }, "labels": { "key": "value" } } }`) }) ctx := context.Background() opts := VolumeCreateOpts{ Name: "my-volume", Size: 42, Server: &Server{ID: 1}, Labels: map[string]string{"key": "value"}, Automount: Bool(true), Format: String("xfs"), } result, _, err := env.Client.Volume.Create(ctx, opts) if err != nil { t.Fatal(err) } if result.Volume.ID != 1 { t.Errorf("unexpected volume ID: %v", result.Volume.ID) } } func TestVolumeClientUpdate(t *testing.T) { var ( ctx = context.Background() volume = &Volume{ID: 1} ) t.Run("name", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.VolumeUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "test" { t.Errorf("unexpected name: %v", reqBody.Name) } json.NewEncoder(w).Encode(schema.VolumeUpdateResponse{ Volume: schema.Volume{ ID: 1, }, }) }) opts := VolumeUpdateOpts{ Name: "test", } updatedVolume, _, err := env.Client.Volume.Update(ctx, volume, opts) if err != nil { t.Fatal(err) } if updatedVolume.ID != 1 { t.Errorf("unexpected volume ID: %v", updatedVolume.ID) } }) t.Run("labels", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.VolumeUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" { t.Errorf("unexpected labels in request: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.VolumeUpdateResponse{ Volume: schema.Volume{ ID: 1, }, }) }) opts := VolumeUpdateOpts{ Name: "test", Labels: map[string]string{"key": "value"}, } updatedVolume, _, err := env.Client.Volume.Update(ctx, volume, opts) if err != nil { t.Fatal(err) } if updatedVolume.ID != 1 { t.Errorf("unexpected volume ID: %v", updatedVolume.ID) } }) t.Run("no updates", func(t *testing.T) { env := newTestEnv() defer env.Teardown() env.Mux.HandleFunc("/volumes/1", func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Error("expected PUT") } var reqBody schema.VolumeUpdateRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } if reqBody.Name != "" { t.Errorf("unexpected no name, but got: %v", reqBody.Name) } if reqBody.Labels != nil { t.Errorf("unexpected no labels, but got: %v", reqBody.Labels) } json.NewEncoder(w).Encode(schema.VolumeUpdateResponse{ Volume: schema.Volume{ ID: 1, }, }) }) opts := VolumeUpdateOpts{} updatedVolume, _, err := env.Client.Volume.Update(ctx, volume, opts) if err != nil { t.Fatal(err) } if updatedVolume.ID != 1 { t.Errorf("unexpected volume ID: %v", updatedVolume.ID) } }) } hcloud-go-1.17.0/script/000077500000000000000000000000001356226054300147735ustar00rootroot00000000000000hcloud-go-1.17.0/script/checkall.bash000077500000000000000000000001661356226054300174060ustar00rootroot00000000000000#!/bin/bash -e cd hcloud diff -u <(echo -n) <(gofmt -d -s .) go vet ./... golint -set_exit_status ./... go test ./...