Repository: hetznercloud/hcloud-go (go)

Issue:
Implement listing actions
https://docs.hetzner.cloud/#resources-actions-get

Pull request:
Implement listing actions
Closes #27.

Patch:
diff --git a/hcloud/action.go b/hcloud/action.go
index e673c3f..673d6b1 100644
--- a/hcloud/action.go
+++ b/hcloud/action.go
@@ -77,3 +77,51 @@ func (c *ActionClient) GetByID(ctx context.Context, id int) (*Action, *Response,
 	}
 	return ActionFromSchema(body.Action), resp, nil
 }
+
+// ActionListOpts specifies options for listing actions.
+type ActionListOpts struct {
+	ListOpts
+}
+
+// List returns a list of actions for a specific page.
+func (c *ActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) {
+	path := "/actions?" + valuesForListOpts(opts.ListOpts).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
+}
diff --git a/hcloud/image.go b/hcloud/image.go
index 7eddc87..d414795 100644
--- a/hcloud/image.go
+++ b/hcloud/image.go
@@ -1,7 +1,9 @@
 package hcloud
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"net/url"
 	"strconv"
@@ -157,3 +159,36 @@ func (c *ImageClient) Delete(ctx context.Context, image *Image) (*Response, erro
 	}
 	return c.client.Do(req, nil)
 }
+
+// ImageUpdateOpts specifies options for updating an image.
+type ImageUpdateOpts struct {
+	Description *string
+	Type        ImageType
+}
+
+// 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))
+	}
+	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
+}
diff --git a/hcloud/schema/action.go b/hcloud/schema/action.go
index c34c74d..df4d7cf 100644
--- a/hcloud/schema/action.go
+++ b/hcloud/schema/action.go
@@ -32,3 +32,8 @@ type ActionError struct {
 type ActionGetResponse struct {
 	Action Action `json:"action"`
 }
+
+// ActionListResponse defines the schema of the response when listing actions.
+type ActionListResponse struct {
+	Actions []Action `json:"actions"`
+}
diff --git a/hcloud/schema/image.go b/hcloud/schema/image.go
index b58e486..1d8c69c 100644
--- a/hcloud/schema/image.go
+++ b/hcloud/schema/image.go
@@ -36,3 +36,14 @@ type ImageGetResponse struct {
 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"`
+}
+
+// ImageUpdateResponse defines the schema of the response when updating an image.
+type ImageUpdateResponse struct {
+	Image Image `json:"image"`
+}
diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go
index 3a5f695..2ee1f4b 100644
--- a/hcloud/schema/server.go
+++ b/hcloud/schema/server.go
@@ -82,6 +82,16 @@ type ServerCreateResponse struct {
 	RootPassword *string `json:"root_password"`
 }
 
+// ServerUpdateRequest defines the schema of the request to update a server.
+type ServerUpdateRequest struct {
+	Name string `json:"name,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{}
@@ -236,3 +246,16 @@ type ServerActionDisableBackupRequest struct{}
 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"`
+}
diff --git a/hcloud/server.go b/hcloud/server.go
index d3733ed..31c68f8 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -285,6 +285,35 @@ func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, e
 	return c.client.Do(req, nil)
 }
 
+// ServerUpdateOpts specifies options for updating a server.
+type ServerUpdateOpts struct {
+	Name 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,
+	}
+	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)
@@ -628,3 +657,38 @@ func (c *ServerClient) DisableBackup(ctx context.Context, server *Server) (*Acti
 	}
 	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
+}


Test patch:
diff --git a/hcloud/action_test.go b/hcloud/action_test.go
index 02b1c4d..5475c68 100644
--- a/hcloud/action_test.go
+++ b/hcloud/action_test.go
@@ -62,3 +62,75 @@ func TestActionClientGetByIDNotFound(t *testing.T) {
 		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)
+		}
+		json.NewEncoder(w).Encode(schema.ActionListResponse{
+			Actions: []schema.Action{
+				{ID: 1},
+				{ID: 2},
+			},
+		})
+	})
+
+	opts := ActionListOpts{}
+	opts.Page = 2
+	opts.PerPage = 50
+
+	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")
+	}
+}
diff --git a/hcloud/image_test.go b/hcloud/image_test.go
index 9b040c0..6725354 100644
--- a/hcloud/image_test.go
+++ b/hcloud/image_test.go
@@ -228,3 +228,85 @@ func TestImageClient(t *testing.T) {
 		}
 	})
 }
+
+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)
+		}
+	})
+}
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index 31a5bd8..c0a17e6 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -447,6 +447,81 @@ func TestServersDelete(t *testing.T) {
 	}
 }
 
+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("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()
@@ -966,3 +1041,80 @@ func TestServerClientDisableBackup(t *testing.T) {
 		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)
+		}
+	})
+}