Repository: hetznercloud/hcloud-go (go)

Issue:
Implement updating a Floating IP’s reverse DNS entries
https://docs.hetzner.cloud/#resources-floating-ip-actions-post-2

Pull request:
Implement changing DNS ptr of a Floating IP
Closes #35.

Patch:
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index 34209d4..03c647f 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -261,3 +261,29 @@ func (c *FloatingIPClient) Unassign(ctx context.Context, floatingIP *FloatingIP)
 	}
 	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
+}
diff --git a/hcloud/schema/floating_ip.go b/hcloud/schema/floating_ip.go
index fcdf9db..f6d3888 100644
--- a/hcloud/schema/floating_ip.go
+++ b/hcloud/schema/floating_ip.go
@@ -78,3 +78,16 @@ type FloatingIPActionUnassignRequest struct{}
 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"`
+}
diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go
index 2ee1f4b..55c60d9 100644
--- a/hcloud/schema/server.go
+++ b/hcloud/schema/server.go
@@ -259,3 +259,16 @@ type ServerActionChangeTypeRequest struct {
 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"`
+}
diff --git a/hcloud/server.go b/hcloud/server.go
index 31c68f8..f5053b1 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -692,3 +692,29 @@ func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts Serv
 	}
 	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
+}


Test patch:
diff --git a/hcloud/floating_ip_test.go b/hcloud/floating_ip_test.go
index 2345c98..f4810dd 100644
--- a/hcloud/floating_ip_test.go
+++ b/hcloud/floating_ip_test.go
@@ -280,3 +280,72 @@ func TestFloatingIPClientUnassign(t *testing.T) {
 		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)
+		}
+	})
+}
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index c0a17e6..67bf42f 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -1118,3 +1118,72 @@ func TestServerClientChangeType(t *testing.T) {
 		}
 	})
 }
+
+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)
+		}
+	})
+}