Repository: hetznercloud/hcloud-go
Pull request #145 (2020-08-07 05:42:19)

Add label selector and ip targets to Load Balancer


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index aac9d21..08ab793 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,10 @@
 # Changes
 
+## Master
+
+* Add support for Load Balancer Label Selector targets
+* Add support for Load Balancer IP targets
+
 ## v1.19.0
 
 * Fix nil pointer dereference when creating a Load Balancer with HTTP(S)
diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go
index 3ba1d39..79fba32 100644
--- a/hcloud/load_balancer.go
+++ b/hcloud/load_balancer.go
@@ -117,8 +117,16 @@ type LoadBalancerAlgorithm struct {
 type LoadBalancerTargetType string
 
 const (
-	// LoadBalancerTargetTypeServer is a target type which points to a specific server.
+	// LoadBalancerTargetTypeServer is a target type which points to a specific
+	// server.
 	LoadBalancerTargetTypeServer LoadBalancerTargetType = "server"
+
+	// LoadBalancerTargetTypeLabelSelector is a target type which selects the
+	// servers a Load Balancer points to using labels assigned to the servers.
+	LoadBalancerTargetTypeLabelSelector LoadBalancerTargetType = "label_selector"
+
+	// LoadBalancerTargetTypeIP is a target type which points to an IP.
+	LoadBalancerTargetTypeIP LoadBalancerTargetType = "ip"
 )
 
 // LoadBalancerServiceProtocol specifies the protocol of a Load Balancer service.
@@ -135,19 +143,34 @@ const (
 
 // LoadBalancerTarget represents a Load Balancer target.
 type LoadBalancerTarget struct {
-	Type         LoadBalancerTargetType
-	Server       *LoadBalancerTargetServer
-	HealthStatus []LoadBalancerTargetHealthStatus
-	Targets      []LoadBalancerTarget
-	UsePrivateIP bool
+	Type          LoadBalancerTargetType
+	Server        *LoadBalancerTargetServer
+	LabelSelector *LoadBalancerTargetLabelSelector
+	IP            *LoadBalancerTargetIP
+	HealthStatus  []LoadBalancerTargetHealthStatus
+	Targets       []LoadBalancerTarget
+	UsePrivateIP  bool
 }
 
 // LoadBalancerTargetServer configures a Load Balancer target
-// pointing to a specific server.
+// pointing at a specific server.
 type LoadBalancerTargetServer struct {
 	Server *Server
 }
 
+// LoadBalancerTargetLabelSelector configures a Load Balancer target pointing
+// at the servers matching the selector. This includes the target pointing at
+// nothing, if no servers match the Selector.
+type LoadBalancerTargetLabelSelector struct {
+	Selector string
+}
+
+// LoadBalancerTargetIP configures a Load Balancer target pointing to a Hetzner
+// Online IP address.
+type LoadBalancerTargetIP struct {
+	IP string
+}
+
 // LoadBalancerTargetHealthStatusStatus describes a target's health status.
 type LoadBalancerTargetHealthStatusStatus string
 
@@ -346,9 +369,11 @@ type LoadBalancerCreateOpts struct {
 // LoadBalancerCreateOptsTarget holds options for specifying a target
 // when creating a new Load Balancer.
 type LoadBalancerCreateOptsTarget struct {
-	Type         LoadBalancerTargetType
-	Server       LoadBalancerCreateOptsTargetServer
-	UsePrivateIP *bool
+	Type          LoadBalancerTargetType
+	Server        LoadBalancerCreateOptsTargetServer
+	LabelSelector LoadBalancerCreateOptsTargetLabelSelector
+	IP            LoadBalancerCreateOptsTargetIP
+	UsePrivateIP  *bool
 }
 
 // LoadBalancerCreateOptsTargetServer holds options for specifying a server target
@@ -357,6 +382,18 @@ type LoadBalancerCreateOptsTargetServer struct {
 	Server *Server
 }
 
+// LoadBalancerCreateOptsTargetLabelSelector holds options for specifying a label selector target
+// when creating a new Load Balancer.
+type LoadBalancerCreateOptsTargetLabelSelector struct {
+	Selector string
+}
+
+// LoadBalancerCreateOptsTargetIP holds options for specifying an IP target
+// when creating a new Load Balancer.
+type LoadBalancerCreateOptsTargetIP struct {
+	IP string
+}
+
 // LoadBalancerCreateOptsService holds options for specifying a service
 // when creating a new Load Balancer.
 type LoadBalancerCreateOptsService struct {
@@ -508,6 +545,62 @@ func (c *LoadBalancerClient) RemoveServerTarget(ctx context.Context, loadBalance
 	return c.removeTarget(ctx, loadBalancer, reqBody)
 }
 
+// LoadBalancerAddLabelSelectorTargetOpts specifies options for adding a label selector target
+// to a Load Balancer.
+type LoadBalancerAddLabelSelectorTargetOpts struct {
+	Selector     string
+	UsePrivateIP *bool
+}
+
+// AddLabelSelectorTarget adds a label selector target to a Load Balancer.
+func (c *LoadBalancerClient) AddLabelSelectorTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddLabelSelectorTargetOpts) (*Action, *Response, error) {
+	reqBody := schema.LoadBalancerActionAddTargetRequest{
+		Type: string(LoadBalancerTargetTypeLabelSelector),
+		LabelSelector: &schema.LoadBalancerActionAddTargetRequestLabelSelector{
+			Selector: opts.Selector,
+		},
+		UsePrivateIP: opts.UsePrivateIP,
+	}
+	return c.addTarget(ctx, loadBalancer, reqBody)
+}
+
+// RemoveLabelSelectorTarget removes a label selector target from a Load Balancer.
+func (c *LoadBalancerClient) RemoveLabelSelectorTarget(ctx context.Context, loadBalancer *LoadBalancer, labelSelector string) (*Action, *Response, error) {
+	reqBody := schema.LoadBalancerActionRemoveTargetRequest{
+		Type: string(LoadBalancerTargetTypeLabelSelector),
+		LabelSelector: &schema.LoadBalancerActionRemoveTargetRequestLabelSelector{
+			Selector: labelSelector,
+		},
+	}
+	return c.removeTarget(ctx, loadBalancer, reqBody)
+}
+
+// LoadBalancerAddIPTargetOpts specifies options for adding an IP target to a
+// Load Balancer.
+type LoadBalancerAddIPTargetOpts struct {
+	IP net.IP
+}
+
+// AddIPTarget adds an IP target to a Load Balancer.
+func (c *LoadBalancerClient) AddIPTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddIPTargetOpts) (*Action, *Response, error) {
+	reqBody := schema.LoadBalancerActionAddTargetRequest{
+		Type: string(LoadBalancerTargetTypeIP),
+		IP:   &schema.LoadBalancerActionAddTargetRequestIP{IP: opts.IP.String()},
+	}
+	return c.addTarget(ctx, loadBalancer, reqBody)
+}
+
+// RemoveIPTarget removes an IP target from a Load Balancer.
+func (c *LoadBalancerClient) RemoveIPTarget(ctx context.Context, loadBalancer *LoadBalancer, ip net.IP) (*Action, *Response, error) {
+	reqBody := schema.LoadBalancerActionRemoveTargetRequest{
+		Type: string(LoadBalancerTargetTypeIP),
+		IP: &schema.LoadBalancerActionRemoveTargetRequestIP{
+			IP: ip.String(),
+		},
+	}
+	return c.removeTarget(ctx, loadBalancer, reqBody)
+}
+
 // LoadBalancerAddServiceOpts specifies options for adding a service to a Load Balancer.
 type LoadBalancerAddServiceOpts struct {
 	Protocol        LoadBalancerServiceProtocol
diff --git a/hcloud/schema.go b/hcloud/schema.go
index e497af6..ba5c117 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -514,9 +514,21 @@ func LoadBalancerTargetFromSchema(s schema.LoadBalancerTarget) LoadBalancerTarge
 			Server: &Server{ID: s.Server.ID},
 		}
 	}
+	if s.LabelSelector != nil {
+		lt.LabelSelector = &LoadBalancerTargetLabelSelector{
+			Selector: s.LabelSelector.Selector,
+		}
+	}
+	if s.IP != nil {
+		lt.IP = &LoadBalancerTargetIP{IP: s.IP.IP}
+	}
+
 	for _, healthStatus := range s.HealthStatus {
 		lt.HealthStatus = append(lt.HealthStatus, LoadBalancerTargetHealthStatusFromSchema(healthStatus))
 	}
+	for _, target := range s.Targets {
+		lt.Targets = append(lt.Targets, LoadBalancerTargetFromSchema(target))
+	}
 	return lt
 }
 
@@ -709,6 +721,12 @@ func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBala
 		case LoadBalancerTargetTypeServer:
 			schemaTarget.Type = string(LoadBalancerTargetTypeServer)
 			schemaTarget.Server = &schema.LoadBalancerCreateRequestTargetServer{ID: target.Server.Server.ID}
+		case LoadBalancerTargetTypeLabelSelector:
+			schemaTarget.Type = string(LoadBalancerTargetTypeLabelSelector)
+			schemaTarget.LabelSelector = &schema.LoadBalancerCreateRequestTargetLabelSelector{Selector: target.LabelSelector.Selector}
+		case LoadBalancerTargetTypeIP:
+			schemaTarget.Type = string(LoadBalancerTargetTypeIP)
+			schemaTarget.IP = &schema.LoadBalancerCreateRequestTargetIP{IP: target.IP.IP}
 		}
 		req.Targets = append(req.Targets, schemaTarget)
 	}
diff --git a/hcloud/schema/load_balancer.go b/hcloud/schema/load_balancer.go
index 97653ab..7c8784f 100644
--- a/hcloud/schema/load_balancer.go
+++ b/hcloud/schema/load_balancer.go
@@ -82,10 +82,13 @@ type LoadBalancerServiceHealthCheckHTTP struct {
 }
 
 type LoadBalancerTarget struct {
-	Type         string                           `json:"type"`
-	Server       *LoadBalancerTargetServer        `json:"server"`
-	HealthStatus []LoadBalancerTargetHealthStatus `json:"health_status"`
-	UsePrivateIP bool                             `json:"use_private_ip"`
+	Type          string                           `json:"type"`
+	Server        *LoadBalancerTargetServer        `json:"server"`
+	LabelSelector *LoadBalancerTargetLabelSelector `json:"label_selector"`
+	IP            *LoadBalancerTargetIP            `json:"ip"`
+	HealthStatus  []LoadBalancerTargetHealthStatus `json:"health_status"`
+	UsePrivateIP  bool                             `json:"use_private_ip"`
+	Targets       []LoadBalancerTarget             `json:"targets,omitempty"`
 }
 
 type LoadBalancerTargetHealthStatus struct {
@@ -97,6 +100,14 @@ type LoadBalancerTargetServer struct {
 	ID int `json:"id"`
 }
 
+type LoadBalancerTargetLabelSelector struct {
+	Selector string `json:"selector"`
+}
+
+type LoadBalancerTargetIP struct {
+	IP string `json:"ip"`
+}
+
 type LoadBalancerListResponse struct {
 	LoadBalancers []LoadBalancer `json:"load_balancers"`
 }
@@ -106,28 +117,48 @@ type LoadBalancerGetResponse struct {
 }
 
 type LoadBalancerActionAddTargetRequest struct {
-	Type         string                                    `json:"type"`
-	Server       *LoadBalancerActionAddTargetRequestServer `json:"server,omitempty"`
-	UsePrivateIP *bool                                     `json:"use_private_ip,omitempty"`
+	Type          string                                           `json:"type"`
+	Server        *LoadBalancerActionAddTargetRequestServer        `json:"server,omitempty"`
+	LabelSelector *LoadBalancerActionAddTargetRequestLabelSelector `json:"label_selector,omitempty"`
+	IP            *LoadBalancerActionAddTargetRequestIP            `json:"ip,omitempty"`
+	UsePrivateIP  *bool                                            `json:"use_private_ip,omitempty"`
 }
 
 type LoadBalancerActionAddTargetRequestServer struct {
 	ID int `json:"id"`
 }
 
+type LoadBalancerActionAddTargetRequestLabelSelector struct {
+	Selector string `json:"selector"`
+}
+
+type LoadBalancerActionAddTargetRequestIP struct {
+	IP string `json:"ip"`
+}
+
 type LoadBalancerActionAddTargetResponse struct {
 	Action Action `json:"action"`
 }
 
 type LoadBalancerActionRemoveTargetRequest struct {
-	Type   string                                       `json:"type"`
-	Server *LoadBalancerActionRemoveTargetRequestServer `json:"server,omitempty"`
+	Type          string                                              `json:"type"`
+	Server        *LoadBalancerActionRemoveTargetRequestServer        `json:"server,omitempty"`
+	LabelSelector *LoadBalancerActionRemoveTargetRequestLabelSelector `json:"label_selector,omitempty"`
+	IP            *LoadBalancerActionRemoveTargetRequestIP            `json:"ip,omitempty"`
 }
 
 type LoadBalancerActionRemoveTargetRequestServer struct {
 	ID int `json:"id"`
 }
 
+type LoadBalancerActionRemoveTargetRequestLabelSelector struct {
+	Selector string `json:"selector"`
+}
+
+type LoadBalancerActionRemoveTargetRequestIP struct {
+	IP string `json:"ip"`
+}
+
 type LoadBalancerActionRemoveTargetResponse struct {
 	Action Action `json:"action"`
 }
@@ -234,15 +265,25 @@ type LoadBalancerCreateRequestAlgorithm struct {
 }
 
 type LoadBalancerCreateRequestTarget struct {
-	Type         string                                 `json:"type"`
-	Server       *LoadBalancerCreateRequestTargetServer `json:"server,omitempty"`
-	UsePrivateIP *bool                                  `json:"use_private_ip,omitempty"`
+	Type          string                                        `json:"type"`
+	Server        *LoadBalancerCreateRequestTargetServer        `json:"server,omitempty"`
+	LabelSelector *LoadBalancerCreateRequestTargetLabelSelector `json:"label_selector,omitempty"`
+	IP            *LoadBalancerCreateRequestTargetIP            `json:"ip,omitempty"`
+	UsePrivateIP  *bool                                         `json:"use_private_ip,omitempty"`
 }
 
 type LoadBalancerCreateRequestTargetServer struct {
 	ID int `json:"id"`
 }
 
+type LoadBalancerCreateRequestTargetLabelSelector struct {
+	Selector string `json:"selector"`
+}
+
+type LoadBalancerCreateRequestTargetIP struct {
+	IP string `json:"ip"`
+}
+
 type LoadBalancerCreateRequestService struct {
 	Protocol        string                                       `json:"protocol"`
 	ListenPort      *int                                         `json:"listen_port,omitempty"`


Test patch:
diff --git a/hcloud/load_balancer_test.go b/hcloud/load_balancer_test.go
index ded879d..c01be4b 100644
--- a/hcloud/load_balancer_test.go
+++ b/hcloud/load_balancer_test.go
@@ -865,3 +865,152 @@ func TestLoadBalancerClientChangeType(t *testing.T) {
 		}
 	})
 }
+
+func TestLoadBalancerClientAddLabelSelectorTarget(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/load_balancers/1/actions/add_target", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != "POST" {
+			t.Error("expected POST")
+		}
+		var reqBody schema.LoadBalancerActionAddTargetRequest
+		if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+			t.Fatal(err)
+		}
+		if reqBody.Type != string(LoadBalancerTargetTypeLabelSelector) {
+			t.Errorf("unexpected type %v", reqBody.Type)
+		}
+		if reqBody.LabelSelector.Selector != "key=value" {
+			t.Errorf("unexpected LabelSelector %v", reqBody.LabelSelector)
+		}
+		if *reqBody.UsePrivateIP != false {
+			t.Errorf("unexpected UsePrivateIP %v", reqBody.UsePrivateIP)
+		}
+		json.NewEncoder(w).Encode(schema.LoadBalancerActionAddTargetResponse{
+			Action: schema.Action{
+				ID: 1,
+			},
+		})
+	})
+
+	ctx := context.Background()
+	action, _, err := env.Client.LoadBalancer.AddLabelSelectorTarget(ctx, &LoadBalancer{ID: 1}, LoadBalancerAddLabelSelectorTargetOpts{
+		Selector:     "key=value",
+		UsePrivateIP: Bool(false),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if action.ID != 1 {
+		t.Errorf("unexpected action ID: %d", action.ID)
+	}
+}
+
+func TestLoadBalancerClientRemoveLabelSelectorTarget(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/load_balancers/1/actions/remove_target", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != "POST" {
+			t.Error("expected POST")
+		}
+		var reqBody schema.LoadBalancerActionRemoveTargetRequest
+		if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+			t.Fatal(err)
+		}
+		if reqBody.Type != string(LoadBalancerTargetTypeLabelSelector) {
+			t.Errorf("unexpected type %v", reqBody.Type)
+		}
+		if reqBody.LabelSelector.Selector != "key=value" {
+			t.Errorf("unexpected LabelSelector %v", reqBody.LabelSelector)
+		}
+		json.NewEncoder(w).Encode(schema.LoadBalancerActionRemoveTargetResponse{
+			Action: schema.Action{
+				ID: 1,
+			},
+		})
+	})
+
+	ctx := context.Background()
+	action, _, err := env.Client.LoadBalancer.RemoveLabelSelectorTarget(ctx, &LoadBalancer{ID: 1}, "key=value")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if action.ID != 1 {
+		t.Errorf("unexpected action ID: %d", action.ID)
+	}
+}
+
+func TestLoadBalancerClientAddIPTarget(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/load_balancers/1/actions/add_target", func(w http.ResponseWriter, r *http.Request) {
+		var reqBody schema.LoadBalancerActionAddTargetRequest
+
+		if r.Method != "POST" {
+			t.Error("expected POST")
+		}
+		if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+			t.Fatal(err)
+		}
+		if reqBody.Type != string(LoadBalancerTargetTypeIP) {
+			t.Errorf("unexpected type %v", reqBody.Type)
+		}
+		if reqBody.IP.IP != "1.2.3.4" {
+			t.Errorf("unexpected IP target %v", reqBody.IP)
+		}
+		json.NewEncoder(w).Encode(schema.LoadBalancerActionAddTargetResponse{
+			Action: schema.Action{
+				ID: 1,
+			},
+		})
+	})
+
+	ctx := context.Background()
+	action, _, err := env.Client.LoadBalancer.AddIPTarget(ctx, &LoadBalancer{ID: 1}, LoadBalancerAddIPTargetOpts{
+		IP: net.ParseIP("1.2.3.4"),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if action.ID != 1 {
+		t.Errorf("unexpected action ID: %d", action.ID)
+	}
+}
+
+func TestLoadBalancerClientRemoveIPTarget(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/load_balancers/1/actions/remove_target", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != "POST" {
+			t.Error("expected POST")
+		}
+		var reqBody schema.LoadBalancerActionRemoveTargetRequest
+		if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+			t.Fatal(err)
+		}
+		if reqBody.Type != string(LoadBalancerTargetTypeIP) {
+			t.Errorf("unexpected type %v", reqBody.Type)
+		}
+		if reqBody.IP.IP != "1.2.3.4" {
+			t.Errorf("unexpected IP %v", reqBody.IP)
+		}
+		json.NewEncoder(w).Encode(schema.LoadBalancerActionRemoveTargetResponse{
+			Action: schema.Action{
+				ID: 1,
+			},
+		})
+	})
+
+	ctx := context.Background()
+	action, _, err := env.Client.LoadBalancer.RemoveIPTarget(ctx, &LoadBalancer{ID: 1}, net.ParseIP("1.2.3.4"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if action.ID != 1 {
+		t.Errorf("unexpected action ID: %d", action.ID)
+	}
+}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index 6be7fde..76c667c 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -1317,6 +1317,27 @@ func TestLoadBalancerFromSchema(t *testing.T) {
 					}
 				],
 				"use_private_ip": false
+			},
+			{
+				"type": "label_selector",
+				"label_selector": {
+					"selector": "lbt"
+				},
+				"targets": [
+					{
+						"type": "server",
+						"server": {
+							"id": 80
+						},
+						"health_status": [
+							{
+								"listen_port": 443,
+								"status": "healthy"
+							}
+						],
+						"use_private_ip": false
+					}
+				]
 			}
 		],
 		"algorithm": {
@@ -1365,7 +1386,7 @@ func TestLoadBalancerFromSchema(t *testing.T) {
 	if len(loadBalancer.Services) != 1 {
 		t.Errorf("unexpected length of Services: %v", len(loadBalancer.Services))
 	}
-	if len(loadBalancer.Targets) != 1 {
+	if len(loadBalancer.Targets) != 2 {
 		t.Errorf("unexpected length of Targets: %v", len(loadBalancer.Targets))
 	}
 	if loadBalancer.Algorithm.Type != "round_robin" {
@@ -1511,6 +1532,9 @@ func TestLoadBalancerTargetFromSchema(t *testing.T) {
 		if loadBalancerTarget.Server == nil || loadBalancerTarget.Server.Server.ID != 80 {
 			t.Errorf("unexpected Server: %v", loadBalancerTarget.Server)
 		}
+		if loadBalancerTarget.LabelSelector != nil {
+			t.Errorf("unexpected LabelSelector.Selector: %v", loadBalancerTarget.LabelSelector)
+		}
 		if loadBalancerTarget.UsePrivateIP {
 			t.Errorf("unexpected UsePrivateIP: %v", loadBalancerTarget.UsePrivateIP)
 		}
@@ -1525,6 +1549,80 @@ func TestLoadBalancerTargetFromSchema(t *testing.T) {
 			}
 		}
 	})
+	t.Run("label_selector target", func(t *testing.T) {
+		data := []byte(`{
+			"type": "label_selector",
+			"label_selector": {
+				"selector": "lbt"
+			},
+			"targets": [
+				{
+					"type": "server",
+					"server": {
+						"id": 80
+					},
+					"health_status": [
+						{
+							"listen_port": 443,
+							"status": "healthy"
+						}
+					]
+				}
+			]
+		}`)
+		var s schema.LoadBalancerTarget
+		if err := json.Unmarshal(data, &s); err != nil {
+			t.Fatal(err)
+		}
+		loadBalancerTarget := LoadBalancerTargetFromSchema(s)
+		if loadBalancerTarget.Type != "label_selector" {
+			t.Errorf("unexpected Type: %v", loadBalancerTarget.Type)
+		}
+		if loadBalancerTarget.LabelSelector == nil || loadBalancerTarget.LabelSelector.Selector != "lbt" {
+			t.Errorf("unexpected LabelSelector: %v", loadBalancerTarget.LabelSelector)
+		}
+		if loadBalancerTarget.Server != nil {
+			t.Errorf("unexpected LabelSelector.Server: %v", loadBalancerTarget.Server)
+		}
+		if len(loadBalancerTarget.Targets) != 1 {
+			t.Errorf("unexpected Targets length: %v", len(loadBalancerTarget.Targets))
+		} else {
+			if loadBalancerTarget.Targets[0].Server == nil || loadBalancerTarget.Targets[0].Server.Server.ID != 80 {
+				t.Errorf("unexpected loadBalancerTarget.Targets[0].Server.Server.ID: %v", loadBalancerTarget.Targets[0].Server.Server.ID)
+			}
+			if len(loadBalancerTarget.Targets[0].HealthStatus) != 1 {
+				t.Errorf("unexpected Targets length: %v", len(loadBalancerTarget.Targets[0].HealthStatus))
+			} else {
+				if loadBalancerTarget.Targets[0].HealthStatus[0].ListenPort != 443 {
+					t.Errorf("unexpected HealthStatus[0].ListenPort: %v", loadBalancerTarget.Targets[0].HealthStatus[0].ListenPort)
+				}
+				if loadBalancerTarget.Targets[0].HealthStatus[0].Status != LoadBalancerTargetHealthStatusStatusHealthy {
+					t.Errorf("unexpected HealthStatus[0].Status: %v", loadBalancerTarget.Targets[0].HealthStatus[0].Status)
+				}
+			}
+		}
+	})
+
+	t.Run("ip target", func(t *testing.T) {
+		var s schema.LoadBalancerTarget
+
+		data := []byte(`{
+			"type": "ip",
+			"ip": {
+				"ip": "1.2.3.4"
+			}
+		}`)
+		if err := json.Unmarshal(data, &s); err != nil {
+			t.Fatal(err)
+		}
+		lbTgt := LoadBalancerTargetFromSchema(s)
+		if lbTgt.Type != LoadBalancerTargetTypeIP {
+			t.Errorf("unexpected Type: %s", lbTgt.Type)
+		}
+		if lbTgt.IP.IP != "1.2.3.4" {
+			t.Errorf("unexpected IP: %s", lbTgt.IP.IP)
+		}
+	})
 }
 
 func TestCertificateFromSchema(t *testing.T) {
@@ -1860,6 +1958,10 @@ func TestLoadBalancerCreateOptsToSchema(t *testing.T) {
 							Server: &Server{ID: 5},
 						},
 					},
+					{
+						Type: LoadBalancerTargetTypeIP,
+						IP:   LoadBalancerCreateOptsTargetIP{IP: "1.2.3.4"},
+					},
 				},
 			},
 			Request: schema.LoadBalancerCreateRequest{
@@ -1910,6 +2012,12 @@ func TestLoadBalancerCreateOptsToSchema(t *testing.T) {
 							ID: 5,
 						},
 					},
+					{
+						Type: "ip",
+						IP: &schema.LoadBalancerCreateRequestTargetIP{
+							IP: "1.2.3.4",
+						},
+					},
 				},
 			},
 		},