Repository: hetznercloud/hcloud-go
Pull request #95 (2018-09-06 07:34:18)

Add support for labels


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index eab213b..fa1d39d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,10 @@
 # Changes
 
+## master
+
+* Add `AllWithOpts()` to server, Floating IP, image, and SSH key client
+* Expose labels of servers, Floating IPs, images, and SSH Keys
+
 ## v1.8.0
 
 * Add `WithPollInterval()` option to `Client` which allows to specify the polling interval
diff --git a/hcloud/client.go b/hcloud/client.go
index e49e8c2..c578da2 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -280,8 +280,9 @@ type Ratelimit struct {
 
 // ListOpts specifies options for listing resources.
 type ListOpts struct {
-	Page    int // Page (starting at 1)
-	PerPage int // Items per page (0 means default)
+	Page          int    // Page (starting at 1)
+	PerPage       int    // Items per page (0 means default)
+	LabelSelector string // Label selector for filtering by labels
 }
 
 func valuesForListOpts(opts ListOpts) url.Values {
@@ -292,5 +293,8 @@ func valuesForListOpts(opts ListOpts) url.Values {
 	if opts.PerPage > 0 {
 		vals.Add("per_page", strconv.Itoa(opts.PerPage))
 	}
+	if len(opts.LabelSelector) > 0 {
+		vals.Add("label_selector", opts.LabelSelector)
+	}
 	return vals
 }
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index a1e8dcf..e1372db 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -23,6 +23,7 @@ type FloatingIP struct {
 	HomeLocation *Location
 	Blocked      bool
 	Protection   FloatingIPProtection
+	Labels       map[string]string
 }
 
 // DNSPtrForIP returns the reverse DNS pointer of the IP address.
@@ -94,10 +95,12 @@ func (c *FloatingIPClient) List(ctx context.Context, opts FloatingIPListOpts) ([
 
 // All returns all Floating IPs.
 func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) {
-	allFloatingIPs := []*FloatingIP{}
+	return c.AllWithOpts(ctx, FloatingIPListOpts{ListOpts{PerPage: 50}})
+}
 
-	opts := FloatingIPListOpts{}
-	opts.PerPage = 50
+// AllWithOpts returns all Floating IPs for the given options.
+func (c *FloatingIPClient) AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) {
+	allFloatingIPs := []*FloatingIP{}
 
 	_, err := c.client.all(func(page int) (*Response, error) {
 		opts.Page = page
@@ -121,6 +124,7 @@ type FloatingIPCreateOpts struct {
 	HomeLocation *Location
 	Server       *Server
 	Description  *string
+	Labels       map[string]string
 }
 
 // Validate checks if options are valid.
@@ -159,6 +163,9 @@ func (c *FloatingIPClient) Create(ctx context.Context, opts FloatingIPCreateOpts
 	if opts.Server != nil {
 		reqBody.Server = Int(opts.Server.ID)
 	}
+	if opts.Labels != nil {
+		reqBody.Labels = &opts.Labels
+	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
 		return FloatingIPCreateResult{}, nil, err
@@ -196,6 +203,7 @@ func (c *FloatingIPClient) Delete(ctx context.Context, floatingIP *FloatingIP) (
 // FloatingIPUpdateOpts specifies options for updating a Floating IP.
 type FloatingIPUpdateOpts struct {
 	Description string
+	Labels      map[string]string
 }
 
 // Update updates a Floating IP.
@@ -203,6 +211,9 @@ func (c *FloatingIPClient) Update(ctx context.Context, floatingIP *FloatingIP, o
 	reqBody := schema.FloatingIPUpdateRequest{
 		Description: opts.Description,
 	}
+	if opts.Labels != nil {
+		reqBody.Labels = &opts.Labels
+	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
 		return nil, nil, err
diff --git a/hcloud/image.go b/hcloud/image.go
index fb5822a..7089fac 100644
--- a/hcloud/image.go
+++ b/hcloud/image.go
@@ -31,6 +31,7 @@ type Image struct {
 
 	Protection ImageProtection
 	Deprecated time.Time // The zero value denotes the image is not deprecated.
+	Labels     map[string]string
 }
 
 // IsDeprecated returns whether the image is deprecated.
@@ -143,10 +144,12 @@ func (c *ImageClient) List(ctx context.Context, opts ImageListOpts) ([]*Image, *
 
 // All returns all images.
 func (c *ImageClient) All(ctx context.Context) ([]*Image, error) {
-	allImages := []*Image{}
+	return c.AllWithOpts(ctx, ImageListOpts{ListOpts{PerPage: 50}})
+}
 
-	opts := ImageListOpts{}
-	opts.PerPage = 50
+// AllWithOpts returns all images for the given options.
+func (c *ImageClient) AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) {
+	allImages := []*Image{}
 
 	_, err := c.client.all(func(page int) (*Response, error) {
 		opts.Page = page
@@ -177,6 +180,7 @@ func (c *ImageClient) Delete(ctx context.Context, image *Image) (*Response, erro
 type ImageUpdateOpts struct {
 	Description *string
 	Type        ImageType
+	Labels      map[string]string
 }
 
 // Update updates an image.
@@ -187,6 +191,9 @@ func (c *ImageClient) Update(ctx context.Context, image *Image, opts ImageUpdate
 	if opts.Type != "" {
 		reqBody.Type = String(string(opts.Type))
 	}
+	if opts.Labels != nil {
+		reqBody.Labels = &opts.Labels
+	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
 		return nil, nil, err
diff --git a/hcloud/schema.go b/hcloud/schema.go
index 1c5fa39..329dbbb 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -61,6 +61,10 @@ func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP {
 	for _, entry := range s.DNSPtr {
 		f.DNSPtr[entry.IP] = entry.DNSPtr
 	}
+	f.Labels = map[string]string{}
+	for key, value := range s.Labels {
+		f.Labels[key] = value
+	}
 	return f
 }
 
@@ -142,6 +146,10 @@ func ServerFromSchema(s schema.Server) *Server {
 	if s.ISO != nil {
 		server.ISO = ISOFromSchema(*s.ISO)
 	}
+	server.Labels = map[string]string{}
+	for key, value := range s.Labels {
+		server.Labels[key] = value
+	}
 	return server
 }
 
@@ -212,12 +220,17 @@ func ServerTypeFromSchema(s schema.ServerType) *ServerType {
 
 // SSHKeyFromSchema converts a schema.SSHKey to a SSHKey.
 func SSHKeyFromSchema(s schema.SSHKey) *SSHKey {
-	return &SSHKey{
+	sshKey := &SSHKey{
 		ID:          s.ID,
 		Name:        s.Name,
 		Fingerprint: s.Fingerprint,
 		PublicKey:   s.PublicKey,
 	}
+	sshKey.Labels = map[string]string{}
+	for key, value := range s.Labels {
+		sshKey.Labels[key] = value
+	}
+	return sshKey
 }
 
 // ImageFromSchema converts a schema.Image to an Image.
@@ -256,6 +269,10 @@ func ImageFromSchema(s schema.Image) *Image {
 			ID: *s.BoundTo,
 		}
 	}
+	i.Labels = map[string]string{}
+	for key, value := range s.Labels {
+		i.Labels[key] = value
+	}
 	return i
 }
 
diff --git a/hcloud/schema/floating_ip.go b/hcloud/schema/floating_ip.go
index 3341c52..95ae205 100644
--- a/hcloud/schema/floating_ip.go
+++ b/hcloud/schema/floating_ip.go
@@ -11,6 +11,7 @@ type FloatingIP struct {
 	HomeLocation Location             `json:"home_location"`
 	Blocked      bool                 `json:"blocked"`
 	Protection   FloatingIPProtection `json:"protection"`
+	Labels       map[string]string    `json:"labels"`
 }
 
 // FloatingIPProtection represents the protection level of a Floating IP.
@@ -33,7 +34,8 @@ type FloatingIPGetResponse struct {
 
 // FloatingIPUpdateRequest defines the schema of the request to update a Floating IP.
 type FloatingIPUpdateRequest struct {
-	Description string `json:"description,omitempty"`
+	Description string             `json:"description,omitempty"`
+	Labels      *map[string]string `json:"labels,omitempty"`
 }
 
 // FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP.
@@ -50,10 +52,11 @@ type FloatingIPListResponse struct {
 // FloatingIPCreateRequest defines the schema of the request to
 // create a Floating IP.
 type FloatingIPCreateRequest struct {
-	Type         string  `json:"type"`
-	HomeLocation *string `json:"home_location,omitempty"`
-	Server       *int    `json:"server,omitempty"`
-	Description  *string `json:"description,omitempty"`
+	Type         string             `json:"type"`
+	HomeLocation *string            `json:"home_location,omitempty"`
+	Server       *int               `json:"server,omitempty"`
+	Description  *string            `json:"description,omitempty"`
+	Labels       *map[string]string `json:"labels,omitempty"`
 }
 
 // FloatingIPCreateResponse defines the schema of the response
diff --git a/hcloud/schema/image.go b/hcloud/schema/image.go
index 2c9c63f..c354d9e 100644
--- a/hcloud/schema/image.go
+++ b/hcloud/schema/image.go
@@ -19,6 +19,7 @@ type Image struct {
 	RapidDeploy bool              `json:"rapid_deploy"`
 	Protection  ImageProtection   `json:"protection"`
 	Deprecated  time.Time         `json:"deprecated"`
+	Labels      map[string]string `json:"labels"`
 }
 
 // ImageProtection represents the protection level of a image.
@@ -46,8 +47,9 @@ type ImageListResponse struct {
 
 // ImageUpdateRequest defines the schema of the request to update an image.
 type ImageUpdateRequest struct {
-	Description *string `json:"description,omitempty"`
-	Type        *string `json:"type,omitempty"`
+	Description *string            `json:"description,omitempty"`
+	Type        *string            `json:"type,omitempty"`
+	Labels      *map[string]string `json:"labels,omitempty"`
 }
 
 // ImageUpdateResponse defines the schema of the response when updating an image.
diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go
index 5e13b59..c315222 100644
--- a/hcloud/schema/server.go
+++ b/hcloud/schema/server.go
@@ -4,22 +4,23 @@ import "time"
 
 // Server defines the schema of a server.
 type Server struct {
-	ID              int              `json:"id"`
-	Name            string           `json:"name"`
-	Status          string           `json:"status"`
-	Created         time.Time        `json:"created"`
-	PublicNet       ServerPublicNet  `json:"public_net"`
-	ServerType      ServerType       `json:"server_type"`
-	IncludedTraffic uint64           `json:"included_traffic"`
-	OutgoingTraffic *uint64          `json:"outgoing_traffic"`
-	IngoingTraffic  *uint64          `json:"ingoing_traffic"`
-	BackupWindow    *string          `json:"backup_window"`
-	RescueEnabled   bool             `json:"rescue_enabled"`
-	ISO             *ISO             `json:"iso"`
-	Locked          bool             `json:"locked"`
-	Datacenter      Datacenter       `json:"datacenter"`
-	Image           *Image           `json:"image"`
-	Protection      ServerProtection `json:"protection"`
+	ID              int               `json:"id"`
+	Name            string            `json:"name"`
+	Status          string            `json:"status"`
+	Created         time.Time         `json:"created"`
+	PublicNet       ServerPublicNet   `json:"public_net"`
+	ServerType      ServerType        `json:"server_type"`
+	IncludedTraffic uint64            `json:"included_traffic"`
+	OutgoingTraffic *uint64           `json:"outgoing_traffic"`
+	IngoingTraffic  *uint64           `json:"ingoing_traffic"`
+	BackupWindow    *string           `json:"backup_window"`
+	RescueEnabled   bool              `json:"rescue_enabled"`
+	ISO             *ISO              `json:"iso"`
+	Locked          bool              `json:"locked"`
+	Datacenter      Datacenter        `json:"datacenter"`
+	Image           *Image            `json:"image"`
+	Protection      ServerProtection  `json:"protection"`
+	Labels          map[string]string `json:"labels"`
 }
 
 // ServerProtection defines the schema of a server's resource protection.
@@ -74,14 +75,15 @@ type ServerListResponse struct {
 // ServerCreateRequest defines the schema for the request to
 // create a server.
 type ServerCreateRequest struct {
-	Name             string      `json:"name"`
-	ServerType       interface{} `json:"server_type"` // int or string
-	Image            interface{} `json:"image"`       // int or string
-	SSHKeys          []int       `json:"ssh_keys,omitempty"`
-	Location         string      `json:"location,omitempty"`
-	Datacenter       string      `json:"datacenter,omitempty"`
-	UserData         string      `json:"user_data,omitempty"`
-	StartAfterCreate *bool       `json:"start_after_create,omitempty"`
+	Name             string             `json:"name"`
+	ServerType       interface{}        `json:"server_type"` // int or string
+	Image            interface{}        `json:"image"`       // int or string
+	SSHKeys          []int              `json:"ssh_keys,omitempty"`
+	Location         string             `json:"location,omitempty"`
+	Datacenter       string             `json:"datacenter,omitempty"`
+	UserData         string             `json:"user_data,omitempty"`
+	StartAfterCreate *bool              `json:"start_after_create,omitempty"`
+	Labels           *map[string]string `json:"labels,omitempty"`
 }
 
 // ServerCreateResponse defines the schema of the response when
@@ -94,7 +96,8 @@ type ServerCreateResponse struct {
 
 // ServerUpdateRequest defines the schema of the request to update a server.
 type ServerUpdateRequest struct {
-	Name string `json:"name,omitempty"`
+	Name   string             `json:"name,omitempty"`
+	Labels *map[string]string `json:"labels,omitempty"`
 }
 
 // ServerUpdateResponse defines the schema of the response when updating a server.
@@ -166,8 +169,9 @@ type ServerActionResetPasswordResponse struct {
 // ServerActionCreateImageRequest defines the schema for the request to
 // create a create_image server action.
 type ServerActionCreateImageRequest struct {
-	Type        *string `json:"type"`
-	Description *string `json:"description"`
+	Type        *string            `json:"type"`
+	Description *string            `json:"description"`
+	Labels      *map[string]string `json:"labels,omitempty"`
 }
 
 // ServerActionCreateImageResponse defines the schema of the response when
diff --git a/hcloud/schema/ssh_key.go b/hcloud/schema/ssh_key.go
index 334d3bb..2f80bac 100644
--- a/hcloud/schema/ssh_key.go
+++ b/hcloud/schema/ssh_key.go
@@ -2,17 +2,19 @@ package schema
 
 // SSHKey defines the schema of a SSH key.
 type SSHKey struct {
-	ID          int    `json:"id"`
-	Name        string `json:"name"`
-	Fingerprint string `json:"fingerprint"`
-	PublicKey   string `json:"public_key"`
+	ID          int               `json:"id"`
+	Name        string            `json:"name"`
+	Fingerprint string            `json:"fingerprint"`
+	PublicKey   string            `json:"public_key"`
+	Labels      map[string]string `json:"labels"`
 }
 
 // SSHKeyCreateRequest defines the schema of the request
 // to create a SSH key.
 type SSHKeyCreateRequest struct {
-	Name      string `json:"name"`
-	PublicKey string `json:"public_key"`
+	Name      string             `json:"name"`
+	PublicKey string             `json:"public_key"`
+	Labels    *map[string]string `json:"labels,omitempty"`
 }
 
 // SSHKeyCreateResponse defines the schema of the response
@@ -35,7 +37,8 @@ type SSHKeyGetResponse struct {
 
 // SSHKeyUpdateRequest defines the schema of the request to update a SSH key.
 type SSHKeyUpdateRequest struct {
-	Name string `json:"name,omitempty"`
+	Name   string             `json:"name,omitempty"`
+	Labels *map[string]string `json:"labels,omitempty"`
 }
 
 // SSHKeyUpdateResponse defines the schema of the response when updating a SSH key.
diff --git a/hcloud/server.go b/hcloud/server.go
index 2ca1501..f6f9e37 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -32,6 +32,7 @@ type Server struct {
 	ISO             *ISO
 	Image           *Image
 	Protection      ServerProtection
+	Labels          map[string]string
 }
 
 // ServerProtection represents the protection level of a server.
@@ -168,10 +169,12 @@ func (c *ServerClient) List(ctx context.Context, opts ServerListOpts) ([]*Server
 
 // All returns all servers.
 func (c *ServerClient) All(ctx context.Context) ([]*Server, error) {
-	allServers := []*Server{}
+	return c.AllWithOpts(ctx, ServerListOpts{ListOpts{PerPage: 50}})
+}
 
-	opts := ServerListOpts{}
-	opts.PerPage = 50
+// AllWithOpts returns all servers for the given options.
+func (c *ServerClient) AllWithOpts(ctx context.Context, opts ServerListOpts) ([]*Server, error) {
+	allServers := []*Server{}
 
 	_, err := c.client.all(func(page int) (*Response, error) {
 		opts.Page = page
@@ -199,6 +202,7 @@ type ServerCreateOpts struct {
 	Datacenter       *Datacenter
 	UserData         string
 	StartAfterCreate *bool
+	Labels           map[string]string
 }
 
 // Validate checks if options are valid.
@@ -245,6 +249,9 @@ func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (Serve
 	} else if opts.Image.Name != "" {
 		reqBody.Image = opts.Image.Name
 	}
+	if opts.Labels != nil {
+		reqBody.Labels = &opts.Labels
+	}
 	for _, sshKey := range opts.SSHKeys {
 		reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID)
 	}
@@ -298,7 +305,8 @@ func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, e
 
 // ServerUpdateOpts specifies options for updating a server.
 type ServerUpdateOpts struct {
-	Name string
+	Name   string
+	Labels map[string]string
 }
 
 // Update updates a server.
@@ -306,6 +314,9 @@ func (c *ServerClient) Update(ctx context.Context, server *Server, opts ServerUp
 	reqBody := schema.ServerUpdateRequest{
 		Name: opts.Name,
 	}
+	if opts.Labels != nil {
+		reqBody.Labels = &opts.Labels
+	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
 		return nil, nil, err
@@ -434,6 +445,7 @@ func (c *ServerClient) ResetPassword(ctx context.Context, server *Server) (Serve
 type ServerCreateImageOpts struct {
 	Type        ImageType
 	Description *string
+	Labels      map[string]string
 }
 
 // Validate checks if options are valid.
@@ -469,6 +481,9 @@ func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *Se
 		if opts.Type != "" {
 			reqBody.Type = String(string(opts.Type))
 		}
+		if opts.Labels != nil {
+			reqBody.Labels = &opts.Labels
+		}
 	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
diff --git a/hcloud/ssh_key.go b/hcloud/ssh_key.go
index 1b22b17..2ab55bf 100644
--- a/hcloud/ssh_key.go
+++ b/hcloud/ssh_key.go
@@ -18,6 +18,7 @@ type SSHKey struct {
 	Name        string
 	Fingerprint string
 	PublicKey   string
+	Labels      map[string]string
 }
 
 // SSHKeyClient is a client for the SSH keys API.
@@ -118,10 +119,12 @@ func (c *SSHKeyClient) List(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey
 
 // All returns all SSH keys.
 func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) {
-	allSSHKeys := []*SSHKey{}
+	return c.AllWithOpts(ctx, SSHKeyListOpts{ListOpts{PerPage: 50}})
+}
 
-	opts := SSHKeyListOpts{}
-	opts.PerPage = 50
+// AllWithOpts returns all SSH keys with the given options.
+func (c *SSHKeyClient) AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) {
+	allSSHKeys := []*SSHKey{}
 
 	_, err := c.client.all(func(page int) (*Response, error) {
 		opts.Page = page
@@ -143,6 +146,7 @@ func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) {
 type SSHKeyCreateOpts struct {
 	Name      string
 	PublicKey string
+	Labels    map[string]string
 }
 
 // Validate checks if options are valid.
@@ -161,11 +165,14 @@ func (c *SSHKeyClient) Create(ctx context.Context, opts SSHKeyCreateOpts) (*SSHK
 	if err := opts.Validate(); err != nil {
 		return nil, nil, err
 	}
-
-	reqBodyData, err := json.Marshal(schema.SSHKeyCreateRequest{
+	reqBody := schema.SSHKeyCreateRequest{
 		Name:      opts.Name,
 		PublicKey: opts.PublicKey,
-	})
+	}
+	if opts.Labels != nil {
+		reqBody.Labels = &opts.Labels
+	}
+	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -194,7 +201,8 @@ func (c *SSHKeyClient) Delete(ctx context.Context, sshKey *SSHKey) (*Response, e
 
 // SSHKeyUpdateOpts specifies options for updating a SSH key.
 type SSHKeyUpdateOpts struct {
-	Name string
+	Name   string
+	Labels map[string]string
 }
 
 // Update updates a SSH key.
@@ -202,6 +210,9 @@ func (c *SSHKeyClient) Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUp
 	reqBody := schema.SSHKeyUpdateRequest{
 		Name: opts.Name,
 	}
+	if opts.Labels != nil {
+		reqBody.Labels = &opts.Labels
+	}
 	reqBodyData, err := json.Marshal(reqBody)
 	if err != nil {
 		return nil, nil, err


Test patch:
diff --git a/hcloud/floating_ip_test.go b/hcloud/floating_ip_test.go
index 8145c51..ab8e593 100644
--- a/hcloud/floating_ip_test.go
+++ b/hcloud/floating_ip_test.go
@@ -93,6 +93,33 @@ func TestFloatingIPClientList(t *testing.T) {
 	}
 }
 
+func TestFloatingIPClientAllWithOpts(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) {
+		if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" {
+			t.Errorf("unexpected label selector: %s", labelSelector)
+		}
+		json.NewEncoder(w).Encode(schema.FloatingIPListResponse{
+			FloatingIPs: []schema.FloatingIP{
+				{ID: 1, Type: "ipv4", IP: "131.232.99.1"},
+				{ID: 2, Type: "ipv4", IP: "131.232.99.1"},
+			},
+		})
+	})
+
+	ctx := context.Background()
+	opts := FloatingIPListOpts{ListOpts{LabelSelector: "key=value"}}
+	floatingIPs, err := env.Client.FloatingIP.AllWithOpts(ctx, opts)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(floatingIPs) != 2 {
+		t.Fatal("expected 2 Floating IPs")
+	}
+}
+
 func TestFloatingIPClientCreate(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
@@ -101,6 +128,13 @@ func TestFloatingIPClientCreate(t *testing.T) {
 		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.Labels == nil || (*reqBody.Labels)["key"] != "value" {
+			t.Errorf("unexpected labels in request: %v", reqBody.Labels)
+		}
 		json.NewEncoder(w).Encode(schema.FloatingIPCreateResponse{
 			FloatingIP: schema.FloatingIP{ID: 1, Type: "ipv4", IP: "131.232.99.1"},
 			Action: &schema.Action{
@@ -114,6 +148,7 @@ func TestFloatingIPClientCreate(t *testing.T) {
 		Description:  String("test"),
 		HomeLocation: &Location{Name: "test"},
 		Server:       &Server{ID: 1},
+		Labels:       map[string]string{"key": "value"},
 	}
 
 	ctx := context.Background()
@@ -189,6 +224,41 @@ func TestFloatingIPClientUpdate(t *testing.T) {
 		}
 	})
 
+	t.Run("update labels", 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.Labels == nil || (*reqBody.Labels)["key"] != "value" {
+				t.Errorf("unexpected labels in request: %v", reqBody.Labels)
+			}
+			json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{
+				FloatingIP: schema.FloatingIP{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := FloatingIPUpdateOpts{
+			Labels: map[string]string{"key": "value"},
+		}
+		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()
diff --git a/hcloud/image_test.go b/hcloud/image_test.go
index 7384f4c..a68ffc0 100644
--- a/hcloud/image_test.go
+++ b/hcloud/image_test.go
@@ -229,6 +229,49 @@ func TestImageClient(t *testing.T) {
 		}
 	})
 
+	t.Run("AllWithOpts", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) {
+			if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" {
+				t.Errorf("unexpected label selector: %s", labelSelector)
+			}
+			w.Header().Set("Content-Type", "application/json")
+			json.NewEncoder(w).Encode(struct {
+				Images []schema.Image `json:"images"`
+				Meta   schema.Meta    `json:"meta"`
+			}{
+				Images: []schema.Image{
+					{ID: 1},
+					{ID: 2},
+					{ID: 3},
+				},
+				Meta: schema.Meta{
+					Pagination: &schema.MetaPagination{
+						Page:         1,
+						LastPage:     1,
+						PerPage:      3,
+						TotalEntries: 3,
+					},
+				},
+			})
+		})
+
+		ctx := context.Background()
+		opts := ImageListOpts{ListOpts{LabelSelector: "key=value"}}
+		images, err := env.Client.Image.AllWithOpts(ctx, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(images) != 3 {
+			t.Fatalf("expected 3 images; got %d", len(images))
+		}
+		if images[0].ID != 1 || images[1].ID != 2 || images[2].ID != 3 {
+			t.Errorf("unexpected images")
+		}
+	})
+
 	t.Run("Delete", func(t *testing.T) {
 		env := newTestEnv()
 		defer env.Teardown()
diff --git a/hcloud/schema/floating_ip_test.go b/hcloud/schema/floating_ip_test.go
new file mode 100644
index 0000000..fcbffcd
--- /dev/null
+++ b/hcloud/schema/floating_ip_test.go
@@ -0,0 +1,54 @@
+package schema
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+)
+
+func TestFloatingIPCreateRequest(t *testing.T) {
+	var (
+		oneLabel    = map[string]string{"foo": "bar"}
+		nilLabels   map[string]string
+		emptyLabels = map[string]string{}
+	)
+
+	testCases := []struct {
+		name string
+		in   FloatingIPCreateRequest
+		out  []byte
+	}{
+		{
+			name: "no labels",
+			in:   FloatingIPCreateRequest{Type: "ipv4"},
+			out:  []byte(`{"type":"ipv4"}`),
+		},
+		{
+			name: "one label",
+			in:   FloatingIPCreateRequest{Type: "ipv4", Labels: &oneLabel},
+			out:  []byte(`{"type":"ipv4","labels":{"foo":"bar"}}`),
+		},
+		{
+			name: "nil labels",
+			in:   FloatingIPCreateRequest{Type: "ipv4", Labels: &nilLabels},
+			out:  []byte(`{"type":"ipv4","labels":null}`),
+		},
+		{
+			name: "empty labels",
+			in:   FloatingIPCreateRequest{Type: "ipv4", Labels: &emptyLabels},
+			out:  []byte(`{"type":"ipv4","labels":{}}`),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			data, err := json.Marshal(testCase.in)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !bytes.Equal(data, testCase.out) {
+				t.Fatalf("output %s does not match %s", data, testCase.out)
+			}
+		})
+	}
+}
diff --git a/hcloud/schema/image_test.go b/hcloud/schema/image_test.go
new file mode 100644
index 0000000..d54983b
--- /dev/null
+++ b/hcloud/schema/image_test.go
@@ -0,0 +1,54 @@
+package schema
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+)
+
+func TestImageUpdateRequest(t *testing.T) {
+	var (
+		oneLabel    = map[string]string{"foo": "bar"}
+		nilLabels   map[string]string
+		emptyLabels = map[string]string{}
+	)
+
+	testCases := []struct {
+		name string
+		in   ImageUpdateRequest
+		out  []byte
+	}{
+		{
+			name: "no labels",
+			in:   ImageUpdateRequest{},
+			out:  []byte(`{}`),
+		},
+		{
+			name: "one label",
+			in:   ImageUpdateRequest{Labels: &oneLabel},
+			out:  []byte(`{"labels":{"foo":"bar"}}`),
+		},
+		{
+			name: "nil labels",
+			in:   ImageUpdateRequest{Labels: &nilLabels},
+			out:  []byte(`{"labels":null}`),
+		},
+		{
+			name: "empty labels",
+			in:   ImageUpdateRequest{Labels: &emptyLabels},
+			out:  []byte(`{"labels":{}}`),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			data, err := json.Marshal(testCase.in)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !bytes.Equal(data, testCase.out) {
+				t.Fatalf("output %s does not match %s", data, testCase.out)
+			}
+		})
+	}
+}
diff --git a/hcloud/schema/server_test.go b/hcloud/schema/server_test.go
new file mode 100644
index 0000000..af2b931
--- /dev/null
+++ b/hcloud/schema/server_test.go
@@ -0,0 +1,54 @@
+package schema
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+)
+
+func TestServerActionCreateImageRequest(t *testing.T) {
+	var (
+		oneLabel    = map[string]string{"foo": "bar"}
+		nilLabels   map[string]string
+		emptyLabels = map[string]string{}
+	)
+
+	testCases := []struct {
+		name string
+		in   ServerActionCreateImageRequest
+		out  []byte
+	}{
+		{
+			name: "no labels",
+			in:   ServerActionCreateImageRequest{},
+			out:  []byte(`{"type":null,"description":null}`),
+		},
+		{
+			name: "one label",
+			in:   ServerActionCreateImageRequest{Labels: &oneLabel},
+			out:  []byte(`{"type":null,"description":null,"labels":{"foo":"bar"}}`),
+		},
+		{
+			name: "nil labels",
+			in:   ServerActionCreateImageRequest{Labels: &nilLabels},
+			out:  []byte(`{"type":null,"description":null,"labels":null}`),
+		},
+		{
+			name: "empty labels",
+			in:   ServerActionCreateImageRequest{Labels: &emptyLabels},
+			out:  []byte(`{"type":null,"description":null,"labels":{}}`),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			data, err := json.Marshal(testCase.in)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !bytes.Equal(data, testCase.out) {
+				t.Fatalf("output %s does not match %s", data, testCase.out)
+			}
+		})
+	}
+}
diff --git a/hcloud/schema/ssh_key_test.go b/hcloud/schema/ssh_key_test.go
new file mode 100644
index 0000000..e0a339d
--- /dev/null
+++ b/hcloud/schema/ssh_key_test.go
@@ -0,0 +1,54 @@
+package schema
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+)
+
+func TestSSHKeyCreateRequest(t *testing.T) {
+	var (
+		oneLabel    = map[string]string{"foo": "bar"}
+		nilLabels   map[string]string
+		emptyLabels = map[string]string{}
+	)
+
+	testCases := []struct {
+		name string
+		in   SSHKeyCreateRequest
+		out  []byte
+	}{
+		{
+			name: "no labels",
+			in:   SSHKeyCreateRequest{Name: "test", PublicKey: "key"},
+			out:  []byte(`{"name":"test","public_key":"key"}`),
+		},
+		{
+			name: "one label",
+			in:   SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &oneLabel},
+			out:  []byte(`{"name":"test","public_key":"key","labels":{"foo":"bar"}}`),
+		},
+		{
+			name: "nil labels",
+			in:   SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &nilLabels},
+			out:  []byte(`{"name":"test","public_key":"key","labels":null}`),
+		},
+		{
+			name: "empty labels",
+			in:   SSHKeyCreateRequest{Name: "test", PublicKey: "key", Labels: &emptyLabels},
+			out:  []byte(`{"name":"test","public_key":"key","labels":{}}`),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			data, err := json.Marshal(testCase.in)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !bytes.Equal(data, testCase.out) {
+				t.Fatalf("output %s does not match %s", data, testCase.out)
+			}
+		})
+	}
+}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index 8ea1e50..e8ef91c 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -91,6 +91,10 @@ func TestFloatingIPFromSchema(t *testing.T) {
 			},
 			"protection": {
 				"delete": true
+			},
+			"labels": {
+				"key": "value",
+				"key2": "value2"
 			}
 		}`)
 
@@ -127,6 +131,9 @@ func TestFloatingIPFromSchema(t *testing.T) {
 		if !floatingIP.Protection.Delete {
 			t.Errorf("unexpected Protection.Delete: %v", floatingIP.Protection.Delete)
 		}
+		if floatingIP.Labels["key"] != "value" || floatingIP.Labels["key2"] != "value2" {
+			t.Errorf("unexpected Labels: %v", floatingIP.Labels)
+		}
 	})
 
 	t.Run("IPv4", func(t *testing.T) {
@@ -341,7 +348,11 @@ func TestServerFromSchema(t *testing.T) {
 			"delete": true,
 			"rebuild": true
 		},
-		"locked": true
+		"locked": true,
+		"labels": {
+			"key": "value",
+			"key2": "value2"
+		}
 	}`)
 
 	var s schema.Server
@@ -401,6 +412,9 @@ func TestServerFromSchema(t *testing.T) {
 	if !server.Protection.Rebuild {
 		t.Errorf("unexpected value for Protection.Rebuild: %v", server.Protection.Rebuild)
 	}
+	if server.Labels["key"] != "value" || server.Labels["key2"] != "value2" {
+		t.Errorf("unexpected Labels: %v", server.Labels)
+	}
 }
 
 func TestServerFromSchemaNoTraffic(t *testing.T) {
@@ -606,7 +620,11 @@ func TestSSHKeyFromSchema(t *testing.T) {
 		"id": 2323,
 		"name": "My key",
 		"fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c",
-		"public_key": "ssh-rsa AAAjjk76kgf...Xt"
+		"public_key": "ssh-rsa AAAjjk76kgf...Xt",
+		"labels": {
+			"key": "value",
+			"key2": "value2"
+		}
 	}`)
 
 	var s schema.SSHKey
@@ -627,6 +645,9 @@ func TestSSHKeyFromSchema(t *testing.T) {
 	if sshKey.PublicKey != "ssh-rsa AAAjjk76kgf...Xt" {
 		t.Errorf("unexpected public key: %v", sshKey.PublicKey)
 	}
+	if sshKey.Labels["key"] != "value" || sshKey.Labels["key2"] != "value2" {
+		t.Errorf("unexpected labels: %v", sshKey.Labels)
+	}
 }
 
 func TestErrorFromSchema(t *testing.T) {
@@ -753,7 +774,11 @@ func TestImageFromSchema(t *testing.T) {
 		"protection": {
 			"delete": true
 		},
-		"deprecated": "2018-02-28T00:00:00+00:00"
+		"deprecated": "2018-02-28T00:00:00+00:00",
+		"labels": {
+			"key": "value",
+			"key2": "value2"
+		}
 	}`)
 
 	var s schema.Image
@@ -807,6 +832,9 @@ func TestImageFromSchema(t *testing.T) {
 	if image.Deprecated.IsZero() {
 		t.Errorf("unexpected value for Deprecated: %v", image.Deprecated)
 	}
+	if image.Labels["key"] != "value" || image.Labels["key2"] != "value2" {
+		t.Errorf("unexpected Labels: %v", image.Labels)
+	}
 }
 
 func TestPricingFromSchema(t *testing.T) {
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index eff7770..5eb74ac 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -210,6 +210,49 @@ func TestServersAll(t *testing.T) {
 	}
 }
 
+func TestServersAllWithOpts(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+		if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" {
+			t.Errorf("unexpected label selector: %s", labelSelector)
+		}
+		w.Header().Set("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(struct {
+			Servers []schema.Server `json:"servers"`
+			Meta    schema.Meta     `json:"meta"`
+		}{
+			Servers: []schema.Server{
+				{ID: 1},
+				{ID: 2},
+				{ID: 3},
+			},
+			Meta: schema.Meta{
+				Pagination: &schema.MetaPagination{
+					Page:         1,
+					LastPage:     1,
+					PerPage:      3,
+					TotalEntries: 3,
+				},
+			},
+		})
+	})
+
+	ctx := context.Background()
+	opts := ServerListOpts{ListOpts{LabelSelector: "key=value"}}
+	servers, err := env.Client.Server.AllWithOpts(ctx, opts)
+	if err != nil {
+		t.Fatalf("Servers.List failed: %s", err)
+	}
+	if len(servers) != 3 {
+		t.Fatalf("expected 3 servers; got %d", len(servers))
+	}
+	if servers[0].ID != 1 || servers[1].ID != 2 || servers[2].ID != 3 {
+		t.Errorf("unexpected servers")
+	}
+}
+
 func TestServersCreateWithSSHKeys(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
@@ -463,6 +506,47 @@ func TestServersCreateWithUserData(t *testing.T) {
 	}
 }
 
+func TestServersCreateWithLabels(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+		var reqBody schema.ServerCreateRequest
+		if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+			t.Fatal(err)
+		}
+		if len(reqBody.SSHKeys) != 0 {
+			t.Errorf("expected no SSH keys, but got %v", reqBody.SSHKeys)
+		}
+		if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" {
+			t.Errorf("unexpected labels in request: %v", reqBody.Labels)
+		}
+		json.NewEncoder(w).Encode(schema.ServerCreateResponse{
+			Server: schema.Server{
+				ID: 1,
+			},
+			RootPassword: String("test"),
+		})
+	})
+
+	ctx := context.Background()
+	result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts{
+		Name:       "test",
+		ServerType: &ServerType{ID: 1},
+		Image:      &Image{ID: 2},
+		Labels:     map[string]string{"key": "value"},
+	})
+	if err != nil {
+		t.Fatalf("Server.Create failed: %s", err)
+	}
+	if result.Server == nil {
+		t.Fatal("no server")
+	}
+	if result.Server.ID != 1 {
+		t.Errorf("unexpected server ID: %v", result.Server.ID)
+	}
+}
+
 func TestServersCreateWithoutStarting(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
@@ -551,6 +635,42 @@ func TestServerClientUpdate(t *testing.T) {
 			t.Fatal(err)
 		}
 
+		if updatedServer.ID != 1 {
+			t.Errorf("unexpected server ID: %v", updatedServer.ID)
+		}
+
+	})
+
+	t.Run("update labels", 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.Labels == nil || (*reqBody.Labels)["key"] != "value" {
+				t.Errorf("unexpected labels in request: %v", reqBody.Labels)
+			}
+			json.NewEncoder(w).Encode(schema.ServerUpdateResponse{
+				Server: schema.Server{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := ServerUpdateOpts{
+			Labels: map[string]string{"key": "value"},
+		}
+		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)
 		}
diff --git a/hcloud/ssh_key_test.go b/hcloud/ssh_key_test.go
index b154cc4..89c6a1c 100644
--- a/hcloud/ssh_key_test.go
+++ b/hcloud/ssh_key_test.go
@@ -290,6 +290,58 @@ func TestSSHKeyAll(t *testing.T) {
 	}
 }
 
+func TestSSHKeyAllWithOpts(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) {
+		if labelSelector := r.URL.Query().Get("label_selector"); labelSelector != "key=value" {
+			t.Errorf("unexpected label selector: %s", labelSelector)
+		}
+		w.Header().Set("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(struct {
+			SSHKeys []schema.SSHKey `json:"ssh_keys"`
+			Meta    schema.Meta     `json:"meta"`
+		}{
+			SSHKeys: []schema.SSHKey{
+				{
+					ID:          1,
+					Name:        "My key",
+					Fingerprint: "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c",
+					PublicKey:   "ssh-rsa AAAjjk76kgf...Xt",
+				},
+				{
+					ID:          2,
+					Name:        "Another key",
+					Fingerprint: "c7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c",
+					PublicKey:   "ssh-rsa AAAjjk76kgf...XX",
+				},
+			},
+			Meta: schema.Meta{
+				Pagination: &schema.MetaPagination{
+					Page:         1,
+					LastPage:     1,
+					PerPage:      2,
+					TotalEntries: 2,
+				},
+			},
+		})
+	})
+
+	ctx := context.Background()
+	opts := SSHKeyListOpts{ListOpts{LabelSelector: "key=value"}}
+	sshKeys, err := env.Client.SSHKey.AllWithOpts(ctx, opts)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(sshKeys) != 2 {
+		t.Fatalf("expected 2 SSH keys; got %d", len(sshKeys))
+	}
+	if sshKeys[0].ID != 1 || sshKeys[1].ID != 2 {
+		t.Error("got wrong SSH keys")
+	}
+}
+
 func TestSSHKeyClientDelete(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
@@ -335,6 +387,43 @@ func TestSSHKeyClientCreate(t *testing.T) {
 	}
 }
 
+func TestSSHKeyClientCreateWithLabels(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/ssh_keys", func(w http.ResponseWriter, r *http.Request) {
+		var reqBody schema.SSHKeyCreateRequest
+		if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+			t.Fatal(err)
+		}
+		if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" {
+			t.Errorf("unexpected labels in request: %v", reqBody.Labels)
+		}
+		fmt.Fprint(w, `{
+			"ssh_key": {
+				"id": 1,
+				"name": "My key",
+				"fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2c",
+				"public_key": "ssh-rsa AAAjjk76kgf...Xt"
+			}
+		}`)
+	})
+
+	ctx := context.Background()
+	opts := SSHKeyCreateOpts{
+		Name:      "My key",
+		PublicKey: "ssh-rsa AAAjjk76kgf...Xt",
+		Labels:    map[string]string{"key": "value"},
+	}
+	sshKey, _, err := env.Client.SSHKey.Create(ctx, opts)
+	if err != nil {
+		t.Fatalf("SSHKey.Get failed: %s", err)
+	}
+	if sshKey.ID != 1 {
+		t.Errorf("unexpected SSH key ID: %v", sshKey.ID)
+	}
+}
+
 func TestSSHKeyClientUpdate(t *testing.T) {
 	var (
 		ctx    = context.Background()
@@ -376,6 +465,42 @@ func TestSSHKeyClientUpdate(t *testing.T) {
 		}
 	})
 
+	t.Run("update labels", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) {
+			if r.Method != "PUT" {
+				t.Error("expected PUT")
+			}
+			var reqBody schema.SSHKeyUpdateRequest
+			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+				t.Fatal(err)
+			}
+			if reqBody.Labels == nil || (*reqBody.Labels)["key"] != "value" {
+				t.Errorf("unexpected labels in request: %v", reqBody.Labels)
+			}
+			json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{
+				SSHKey: schema.SSHKey{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := SSHKeyUpdateOpts{
+			Name:   "test",
+			Labels: map[string]string{"key": "value"},
+		}
+		updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if updatedSSHKey.ID != 1 {
+			t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID)
+		}
+	})
+
 	t.Run("no updates", func(t *testing.T) {
 		env := newTestEnv()
 		defer env.Teardown()