Repository: hetznercloud/hcloud-go (go)

Issue:
Implement updating an image
https://docs.hetzner.cloud/#resources-images-put

Pull request description:
Implement updating an image
Closes #37.

Patch:
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/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..91d0615 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{}
diff --git a/hcloud/server.go b/hcloud/server.go
index d3733ed..8b6bbc7 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)


Test patch:
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..f963a5a 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()