Repository: hetznercloud/hcloud-go
Pull request #39 (2018-01-10 10:17:04)

Add Get and GetByName to ServerTypeClient


Patch:
diff --git a/hcloud/client.go b/hcloud/client.go
index eb8d5a9..1bfc7a3 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -55,6 +55,7 @@ type Client struct {
 	Datacenter DatacenterClient
 	FloatingIP FloatingIPClient
 	Image      ImageClient
+	ISO        ISOClient
 	Location   LocationClient
 	Server     ServerClient
 	ServerType ServerTypeClient
@@ -101,6 +102,7 @@ func NewClient(options ...ClientOption) *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.Server = ServerClient{client: client}
 	client.ServerType = ServerTypeClient{client: client}
diff --git a/hcloud/iso.go b/hcloud/iso.go
index ec6fa9c..73b2387 100644
--- a/hcloud/iso.go
+++ b/hcloud/iso.go
@@ -1,5 +1,14 @@
 package hcloud
 
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"strconv"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
 // ISO represents an ISO image in the Hetzner Cloud.
 type ISO struct {
 	ID          int
@@ -18,3 +27,102 @@ const (
 	// ISOTypePrivate is the type of a private ISO image.
 	ISOTypePrivate = "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) {
+	path := "/isos?name=" + url.QueryEscape(name)
+	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
+	}
+
+	if len(body.ISOs) == 0 {
+		return nil, resp, nil
+	}
+	return ISOFromSchema(body.ISOs[0]), resp, nil
+}
+
+// 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
+}
+
+// List returns a list of ISOs for a specific page.
+func (c *ISOClient) List(ctx context.Context, opts ISOListOpts) ([]*ISO, *Response, error) {
+	path := "/isos?" + valuesForListOpts(opts.ListOpts).Encode()
+	req, err := c.client.NewRequest(ctx, "GET", path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.ISOListResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return nil, nil, err
+	}
+	isos := make([]*ISO, 0, len(body.ISOs))
+	for _, i := range body.ISOs {
+		isos = append(isos, ISOFromSchema(i))
+	}
+	return isos, resp, nil
+}
+
+// All returns all ISOs.
+func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) {
+	allISOs := []*ISO{}
+
+	opts := ISOListOpts{}
+	opts.PerPage = 50
+
+	_, err := c.client.all(func(page int) (*Response, error) {
+		opts.Page = page
+		isos, resp, err := c.List(ctx, opts)
+		if err != nil {
+			return resp, err
+		}
+		allISOs = append(allISOs, isos...)
+		return resp, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return allISOs, nil
+}
diff --git a/hcloud/schema/iso.go b/hcloud/schema/iso.go
index e217dcd..cdf8e96 100644
--- a/hcloud/schema/iso.go
+++ b/hcloud/schema/iso.go
@@ -7,3 +7,13 @@ type ISO struct {
 	Description string `json:"description"`
 	Type        string `json:"type"`
 }
+
+// ISOGetResponse defines the schema of the response when retrieving a single ISO.
+type ISOGetResponse struct {
+	ISO ISO `json:"iso"`
+}
+
+// ISOListResponse defines the schema of the response when listing ISOs.
+type ISOListResponse struct {
+	ISOs []ISO `json:"isos"`
+}
diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go
index 5d6c6c0..41e2e22 100644
--- a/hcloud/schema/server.go
+++ b/hcloud/schema/server.go
@@ -180,3 +180,15 @@ type ServerActionDisableRescueRequest struct{}
 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"`
+}
diff --git a/hcloud/server.go b/hcloud/server.go
index 664d3ea..636ba59 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -510,3 +510,35 @@ func (c *ServerClient) DisableRescue(ctx context.Context, server *Server) (*Acti
 	}
 	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
+}
diff --git a/hcloud/server_type.go b/hcloud/server_type.go
index 929799b..d680e9a 100644
--- a/hcloud/server_type.go
+++ b/hcloud/server_type.go
@@ -3,6 +3,8 @@ package hcloud
 import (
 	"context"
 	"fmt"
+	"net/url"
+	"strconv"
 
 	"github.com/hetznercloud/hcloud-go/hcloud/schema"
 )
@@ -52,6 +54,34 @@ func (c *ServerTypeClient) GetByID(ctx context.Context, id int) (*ServerType, *R
 	return ServerTypeFromSchema(body.ServerType), resp, nil
 }
 
+// GetByName retrieves a server type by its name.
+func (c *ServerTypeClient) GetByName(ctx context.Context, name string) (*ServerType, *Response, error) {
+	path := "/server_types?name=" + url.QueryEscape(name)
+	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
+	}
+
+	if len(body.ServerTypes) == 0 {
+		return nil, resp, nil
+	}
+	return ServerTypeFromSchema(body.ServerTypes[0]), resp, nil
+}
+
+// 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.
+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


Test patch:
diff --git a/hcloud/iso_test.go b/hcloud/iso_test.go
new file mode 100644
index 0000000..d005c6e
--- /dev/null
+++ b/hcloud/iso_test.go
@@ -0,0 +1,212 @@
+package hcloud
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
+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: 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")
+		}
+	})
+}
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index 70667a0..9db93e5 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -717,3 +717,72 @@ func TestServerClientDisableRescue(t *testing.T) {
 		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)
+		}
+	})
+}
diff --git a/hcloud/server_type_test.go b/hcloud/server_type_test.go
index 7c4d651..6c9fe7b 100644
--- a/hcloud/server_type_test.go
+++ b/hcloud/server_type_test.go
@@ -33,6 +33,19 @@ func TestServerTypeClient(t *testing.T) {
 		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) {
@@ -59,6 +72,72 @@ func TestServerTypeClient(t *testing.T) {
 		}
 	})
 
+	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()