Repository: hetznercloud/hcloud-go
Pull request #120 (2019-09-16 08:01:54)

Add support for names to Floating IPs


Patch:
diff --git a/.travis.yml b/.travis.yml
index d90a5fe..b91ce2e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,7 @@ go:
   - "1.10"
   - "1.11"
   - "1.12"
+  - "1.13"
 before_script:
   - go get golang.org/x/lint/golint
 script:
diff --git a/CHANGES.md b/CHANGES.md
index 4179eb5..32dbdf4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,6 +3,7 @@
 ## master
 
 * Make IP range optional when adding a subnet to a network
+* Add support for names to Floating IPs
 
 ## v1.15.1
 
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index afeeca9..64fad61 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"net"
 	"net/url"
+	"strconv"
 	"time"
 
 	"github.com/hetznercloud/hcloud-go/hcloud/schema"
@@ -27,6 +28,7 @@ type FloatingIP struct {
 	Blocked      bool
 	Protection   FloatingIPProtection
 	Labels       map[string]string
+	Name         string
 }
 
 // DNSPtrForIP returns the reverse DNS pointer of the IP address.
@@ -72,13 +74,36 @@ func (c *FloatingIPClient) GetByID(ctx context.Context, id int) (*FloatingIP, *R
 	return FloatingIPFromSchema(body.FloatingIP), resp, nil
 }
 
+// GetByName retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned.
+func (c *FloatingIPClient) GetByName(ctx context.Context, name string) (*FloatingIP, *Response, error) {
+	floatingIPs, response, err := c.List(ctx, FloatingIPListOpts{Name: name})
+	if len(floatingIPs) == 0 {
+		return nil, response, err
+	}
+	return floatingIPs[0], response, err
+}
+
+// Get retrieves a Floating IP by its ID if the input can be parsed as an integer, otherwise it
+// retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned.
+func (c *FloatingIPClient) Get(ctx context.Context, idOrName string) (*FloatingIP, *Response, error) {
+	if id, err := strconv.Atoi(idOrName); err == nil {
+		return c.GetByID(ctx, int(id))
+	}
+	return c.GetByName(ctx, idOrName)
+}
+
 // FloatingIPListOpts specifies options for listing Floating IPs.
 type FloatingIPListOpts struct {
 	ListOpts
+	Name string
 }
 
 func (l FloatingIPListOpts) values() url.Values {
-	return l.ListOpts.values()
+	vals := l.ListOpts.values()
+	if l.Name != "" {
+		vals.Add("name", l.Name)
+	}
+	return vals
 }
 
 // List returns a list of Floating IPs for a specific page.
@@ -103,7 +128,7 @@ func (c *FloatingIPClient) List(ctx context.Context, opts FloatingIPListOpts) ([
 
 // All returns all Floating IPs.
 func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) {
-	return c.AllWithOpts(ctx, FloatingIPListOpts{ListOpts{PerPage: 50}})
+	return c.AllWithOpts(ctx, FloatingIPListOpts{ListOpts: ListOpts{PerPage: 50}})
 }
 
 // AllWithOpts returns all Floating IPs for the given options.
@@ -132,6 +157,7 @@ type FloatingIPCreateOpts struct {
 	HomeLocation *Location
 	Server       *Server
 	Description  *string
+	Name         *string
 	Labels       map[string]string
 }
 
@@ -164,6 +190,7 @@ func (c *FloatingIPClient) Create(ctx context.Context, opts FloatingIPCreateOpts
 	reqBody := schema.FloatingIPCreateRequest{
 		Type:        string(opts.Type),
 		Description: opts.Description,
+		Name:        opts.Name,
 	}
 	if opts.HomeLocation != nil {
 		reqBody.HomeLocation = String(opts.HomeLocation.Name)
@@ -212,12 +239,14 @@ func (c *FloatingIPClient) Delete(ctx context.Context, floatingIP *FloatingIP) (
 type FloatingIPUpdateOpts struct {
 	Description string
 	Labels      map[string]string
+	Name        string
 }
 
 // Update updates a Floating IP.
 func (c *FloatingIPClient) Update(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPUpdateOpts) (*FloatingIP, *Response, error) {
 	reqBody := schema.FloatingIPUpdateRequest{
 		Description: opts.Description,
+		Name:        opts.Name,
 	}
 	if opts.Labels != nil {
 		reqBody.Labels = &opts.Labels
diff --git a/hcloud/schema.go b/hcloud/schema.go
index 2b69605..3cf885b 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -55,6 +55,7 @@ func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP {
 		Protection: FloatingIPProtection{
 			Delete: s.Protection.Delete,
 		},
+		Name: s.Name,
 	}
 	if s.Description != nil {
 		f.Description = *s.Description
diff --git a/hcloud/schema/floating_ip.go b/hcloud/schema/floating_ip.go
index 4c56d87..37295da 100644
--- a/hcloud/schema/floating_ip.go
+++ b/hcloud/schema/floating_ip.go
@@ -15,6 +15,7 @@ type FloatingIP struct {
 	Blocked      bool                 `json:"blocked"`
 	Protection   FloatingIPProtection `json:"protection"`
 	Labels       map[string]string    `json:"labels"`
+	Name         string               `json:"name"`
 }
 
 // FloatingIPProtection represents the protection level of a Floating IP.
@@ -39,6 +40,7 @@ type FloatingIPGetResponse struct {
 type FloatingIPUpdateRequest struct {
 	Description string             `json:"description,omitempty"`
 	Labels      *map[string]string `json:"labels,omitempty"`
+	Name        string             `json:"name,omitempty"`
 }
 
 // FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP.
@@ -60,6 +62,7 @@ type FloatingIPCreateRequest struct {
 	Server       *int               `json:"server,omitempty"`
 	Description  *string            `json:"description,omitempty"`
 	Labels       *map[string]string `json:"labels,omitempty"`
+	Name         *string            `json:"name,omitempty"`
 }
 
 // FloatingIPCreateResponse defines the schema of the response


Test patch:
diff --git a/hcloud/floating_ip_test.go b/hcloud/floating_ip_test.go
index ab8e593..2cb741a 100644
--- a/hcloud/floating_ip_test.go
+++ b/hcloud/floating_ip_test.go
@@ -34,6 +34,18 @@ func TestFloatingIPClientGetByID(t *testing.T) {
 	if floatingIP.ID != 1 {
 		t.Errorf("unexpected ID: %v", floatingIP.ID)
 	}
+	t.Run("via Get", func(t *testing.T) {
+		floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "1")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if floatingIP == nil {
+			t.Fatal("no Floating IP")
+		}
+		if floatingIP.ID != 1 {
+			t.Errorf("unexpected Floating IP ID: %v", floatingIP.ID)
+		}
+	})
 }
 
 func TestFloatingIPClientGetByIDNotFound(t *testing.T) {
@@ -60,6 +72,83 @@ func TestFloatingIPClientGetByIDNotFound(t *testing.T) {
 	}
 }
 
+func TestFloatingIPClientGetByName(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.RawQuery != "name=myFloatingIP" {
+			t.Fatal("missing name query")
+		}
+		json.NewEncoder(w).Encode(schema.FloatingIPListResponse{
+			FloatingIPs: []schema.FloatingIP{
+				{
+					ID:   1,
+					Name: "myFloatingIP",
+					Type: "ipv4",
+					IP:   "131.232.99.1",
+				},
+			},
+		})
+	})
+
+	ctx := context.Background()
+	floatingIP, _, err := env.Client.FloatingIP.GetByName(ctx, "myFloatingIP")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if floatingIP == nil {
+		t.Fatal("no Floating IP")
+	}
+	if floatingIP.ID != 1 {
+		t.Errorf("unexpected ID: %v", floatingIP.ID)
+	}
+	t.Run("via Get", func(t *testing.T) {
+		floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "myFloatingIP")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if floatingIP == nil {
+			t.Fatal("no Floating IP")
+		}
+		if floatingIP.ID != 1 {
+			t.Errorf("unexpected Floating IP ID: %v", floatingIP.ID)
+		}
+	})
+}
+
+func TestFloatingIPClientGetByNameNotFound(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.RawQuery != "name=myFloatingIP" {
+			t.Fatal("missing name query")
+		}
+		json.NewEncoder(w).Encode(schema.FloatingIPListResponse{
+			FloatingIPs: []schema.FloatingIP{},
+		})
+	})
+
+	ctx := context.Background()
+	floatingIP, _, err := env.Client.FloatingIP.GetByName(ctx, "myFloatingIP")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if floatingIP != nil {
+		t.Fatal("expected no Floating IP")
+	}
+	t.Run("via Get", func(t *testing.T) {
+		floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "myFloatingIP")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if floatingIP != nil {
+			t.Fatal("expected no Floating IP")
+		}
+	})
+}
+
 func TestFloatingIPClientList(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
@@ -110,7 +199,7 @@ func TestFloatingIPClientAllWithOpts(t *testing.T) {
 	})
 
 	ctx := context.Background()
-	opts := FloatingIPListOpts{ListOpts{LabelSelector: "key=value"}}
+	opts := FloatingIPListOpts{ListOpts: ListOpts{LabelSelector: "key=value"}}
 	floatingIPs, err := env.Client.FloatingIP.AllWithOpts(ctx, opts)
 	if err != nil {
 		t.Fatal(err)
@@ -165,6 +254,52 @@ func TestFloatingIPClientCreate(t *testing.T) {
 	}
 }
 
+func TestFloatingIPClientCreateWithName(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != "POST" {
+			t.Error("expected POST")
+		}
+		var reqBody schema.FloatingIPCreateRequest
+		if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+			t.Fatal(err)
+		}
+		if reqBody.Name == nil || *reqBody.Name != "MyFloatingIP" {
+			t.Errorf("unexpected name in request: %v", reqBody.Name)
+		}
+		json.NewEncoder(w).Encode(schema.FloatingIPCreateResponse{
+			FloatingIP: schema.FloatingIP{ID: 1, Type: "ipv4", IP: "131.232.99.1"},
+			Action: &schema.Action{
+				ID: 1,
+			},
+		})
+	})
+
+	opts := FloatingIPCreateOpts{
+		Type:         FloatingIPTypeIPv4,
+		Description:  String("test"),
+		HomeLocation: &Location{Name: "test"},
+		Server:       &Server{ID: 1},
+		Name:         String("MyFloatingIP"),
+		Labels:       map[string]string{"key": "value"},
+	}
+
+	ctx := context.Background()
+	result, _, err := env.Client.FloatingIP.Create(ctx, opts)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if result.FloatingIP.ID != 1 {
+		t.Errorf("unexpected Floating IP ID: %d", result.FloatingIP.ID)
+	}
+	if result.Action.ID != 1 {
+		t.Errorf("unexpected action ID: %d", result.Action.ID)
+	}
+}
+
 func TestFloatingIPClientDelete(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
@@ -259,6 +394,41 @@ func TestFloatingIPClientUpdate(t *testing.T) {
 		}
 	})
 
+	t.Run("update name", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) {
+			if r.Method != "PUT" {
+				t.Error("expected PUT")
+			}
+			var reqBody schema.FloatingIPUpdateRequest
+			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.FloatingIPUpdateResponse{
+				FloatingIP: schema.FloatingIP{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := FloatingIPUpdateOpts{
+			Name: "test",
+		}
+		updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if updatedFloatingIP.ID != 1 {
+			t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID)
+		}
+	})
+
 	t.Run("no updates", func(t *testing.T) {
 		env := newTestEnv()
 		defer env.Teardown()
@@ -274,6 +444,9 @@ func TestFloatingIPClientUpdate(t *testing.T) {
 			if reqBody.Description != "" {
 				t.Errorf("unexpected no description, but got: %v", reqBody.Description)
 			}
+			if reqBody.Name != "" {
+				t.Errorf("unexpected no name, but got: %v", reqBody.Name)
+			}
 			json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{
 				FloatingIP: schema.FloatingIP{
 					ID: 1,
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index 55e1582..8042a7d 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -95,6 +95,7 @@ func TestFloatingIPFromSchema(t *testing.T) {
 	t.Run("IPv6", func(t *testing.T) {
 		data := []byte(`{
 			"id": 4711,
+			"name": "Web Frontend",
 			"description": "Web Frontend",
 			"created":"2017-08-16T17:29:14+00:00",
 			"ip": "2001:db8::/64",
@@ -133,6 +134,9 @@ func TestFloatingIPFromSchema(t *testing.T) {
 		if !floatingIP.Blocked {
 			t.Errorf("unexpected value for Blocked: %v", floatingIP.Blocked)
 		}
+		if floatingIP.Name != "Web Frontend" {
+			t.Errorf("unexpected name: %v", floatingIP.Name)
+		}
 		if floatingIP.Description != "Web Frontend" {
 			t.Errorf("unexpected description: %v", floatingIP.Description)
 		}