Repository: hetznercloud/hcloud-go (go)

Issue:
Implement attaching and detaching ISOs
https://docs.hetzner.cloud/#resources-server-actions-post-13

Pull request description:
Implement attaching and detaching ISOs
Closes #32.

Patch:
diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go
index 41e2e22..1903b6e 100644
--- a/hcloud/schema/server.go
+++ b/hcloud/schema/server.go
@@ -192,3 +192,25 @@ type ServerActionRebuildRequest struct {
 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"`
+}
diff --git a/hcloud/server.go b/hcloud/server.go
index 636ba59..0dd6650 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -542,3 +542,46 @@ func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerR
 	}
 	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
+}


Test patch:
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index 9db93e5..84fd37c 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -786,3 +786,94 @@ func TestServerClientRebuild(t *testing.T) {
 		}
 	})
 }
+
+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)
+	}
+}