Repository: hetznercloud/hcloud-go
Pull request #268 (2023-06-05 12:56:21)

feat(servertype): implement new Deprecation api field
We recently added a new field to the ServerType API response that indicates if and when the server type was deprecated. The same structure will also be used for other resources in the future, replacing the current `deprecated` fields.

Marked as draft until I have finished the implementation in our other Go Integrations, to make sure that this feels ergonomic.


Patch:
diff --git a/hcloud/deprecation.go b/hcloud/deprecation.go
new file mode 100644
index 0000000..17c6949
--- /dev/null
+++ b/hcloud/deprecation.go
@@ -0,0 +1,59 @@
+package hcloud
+
+import "time"
+
+// Deprecatable is a shared interface implemented by all Resources that have a defined deprecation workflow.
+type Deprecatable interface {
+	// IsDeprecated returns true if the resource is marked as deprecated.
+	IsDeprecated() bool
+
+	// UnavailableAfter returns the time that the deprecated resource will be removed from the API.
+	// This only returns a valid value if [Deprecatable.IsDeprecated] returned true.
+	UnavailableAfter() time.Time
+
+	// DeprecationAnnounced returns the time that the deprecation of this resource was announced.
+	// This only returns a valid value if [Deprecatable.IsDeprecated] returned true.
+	DeprecationAnnounced() time.Time
+}
+
+// DeprecationInfo contains the information published when a resource is actually deprecated.
+type DeprecationInfo struct {
+	Announced        time.Time
+	UnavailableAfter time.Time
+}
+
+// DeprecatableResource implements the [Deprecatable] interface and can be embedded in structs for Resources that can be
+// deprecated.
+type DeprecatableResource struct {
+	Deprecation *DeprecationInfo
+}
+
+// IsDeprecated returns true if the resource is marked as deprecated.
+func (d DeprecatableResource) IsDeprecated() bool {
+	return d.Deprecation != nil
+}
+
+// UnavailableAfter returns the time that the deprecated resource will be removed from the API.
+// This only returns a valid value if [Deprecatable.IsDeprecated] returned true.
+func (d DeprecatableResource) UnavailableAfter() time.Time {
+	if !d.IsDeprecated() {
+		// Return "null" time if resource is not deprecated
+		return time.Unix(0, 0)
+	}
+
+	return d.Deprecation.UnavailableAfter
+}
+
+// DeprecationAnnounced returns the time that the deprecation of this resource was announced.
+// This only returns a valid value if [Deprecatable.IsDeprecated] returned true.
+func (d DeprecatableResource) DeprecationAnnounced() time.Time {
+	if !d.IsDeprecated() {
+		// Return "null" time if resource is not deprecated
+		return time.Unix(0, 0)
+	}
+
+	return d.Deprecation.Announced
+}
+
+// Make sure that all expected Resources actually implement the interface.
+var _ Deprecatable = ServerType{}
diff --git a/hcloud/rdns.go b/hcloud/rdns.go
index 891ea2b..f53c030 100644
--- a/hcloud/rdns.go
+++ b/hcloud/rdns.go
@@ -7,7 +7,7 @@ import (
 )
 
 // RDNSSupporter defines functions to change and lookup reverse dns entries.
-// currently implemented by Server, FloatingIP and LoadBalancer.
+// currently implemented by Server, FloatingIP, PrimaryIP and LoadBalancer.
 type RDNSSupporter interface {
 	// changeDNSPtr changes or resets the reverse DNS pointer for a IP address.
 	// Pass a nil ptr to reset the reverse DNS pointer to its default value.
@@ -17,7 +17,7 @@ type RDNSSupporter interface {
 	GetDNSPtrForIP(ip net.IP) (string, error)
 }
 
-// RDNSClient simplifys the handling objects which support reverse dns entries.
+// RDNSClient simplifies the handling objects which support reverse dns entries.
 type RDNSClient struct {
 	client *Client
 }
@@ -44,3 +44,9 @@ func RDNSLookup(i interface{}, ip net.IP) (string, error) {
 
 	return rdns.GetDNSPtrForIP(ip)
 }
+
+// Make sure that all expected Resources actually implement the interface.
+var _ RDNSSupporter = &FloatingIP{}
+var _ RDNSSupporter = &PrimaryIP{}
+var _ RDNSSupporter = &Server{}
+var _ RDNSSupporter = &LoadBalancer{}
diff --git a/hcloud/schema.go b/hcloud/schema.go
index fb2079f..c76d7de 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -288,6 +288,9 @@ func ServerTypeFromSchema(s schema.ServerType) *ServerType {
 		CPUType:         CPUType(s.CPUType),
 		Architecture:    Architecture(s.Architecture),
 		IncludedTraffic: s.IncludedTraffic,
+		DeprecatableResource: DeprecatableResource{
+			DeprecationFromSchema(s.Deprecation),
+		},
 	}
 	for _, price := range s.Prices {
 		st.Pricings = append(st.Pricings, ServerTypeLocationPricing{
@@ -302,6 +305,7 @@ func ServerTypeFromSchema(s schema.ServerType) *ServerType {
 			},
 		})
 	}
+
 	return st
 }
 
@@ -1249,3 +1253,15 @@ func loadBalancerMetricsFromSchema(s *schema.LoadBalancerGetMetricsResponse) (*L
 
 	return &ms, nil
 }
+
+// DeprecationFromSchema converts a [schema.DeprecationInfo] to a [DeprecationInfo].
+func DeprecationFromSchema(s *schema.DeprecationInfo) *DeprecationInfo {
+	if s == nil {
+		return nil
+	}
+
+	return &DeprecationInfo{
+		Announced:        s.Announced,
+		UnavailableAfter: s.UnavailableAfter,
+	}
+}
diff --git a/hcloud/schema/deprecation.go b/hcloud/schema/deprecation.go
new file mode 100644
index 0000000..87292f7
--- /dev/null
+++ b/hcloud/schema/deprecation.go
@@ -0,0 +1,12 @@
+package schema
+
+import "time"
+
+type DeprecationInfo struct {
+	Announced        time.Time `json:"announced"`
+	UnavailableAfter time.Time `json:"unavailable_after"`
+}
+
+type DeprecatableResource struct {
+	Deprecation *DeprecationInfo `json:"deprecation"`
+}
diff --git a/hcloud/schema/server_type.go b/hcloud/schema/server_type.go
index 1294a00..e4d9b3a 100644
--- a/hcloud/schema/server_type.go
+++ b/hcloud/schema/server_type.go
@@ -13,6 +13,7 @@ type ServerType struct {
 	Architecture    string                   `json:"architecture"`
 	IncludedTraffic int64                    `json:"included_traffic"`
 	Prices          []PricingServerTypePrice `json:"prices"`
+	DeprecatableResource
 }
 
 // ServerTypeListResponse defines the schema of the response when
diff --git a/hcloud/server_type.go b/hcloud/server_type.go
index 1ac0128..c5e0325 100644
--- a/hcloud/server_type.go
+++ b/hcloud/server_type.go
@@ -23,6 +23,7 @@ type ServerType struct {
 	// IncludedTraffic is the free traffic per month in bytes
 	IncludedTraffic int64
 	Pricings        []ServerTypeLocationPricing
+	DeprecatableResource
 }
 
 // StorageType specifies the type of storage.


Test patch:
diff --git a/hcloud/deprecation_test.go b/hcloud/deprecation_test.go
new file mode 100644
index 0000000..e98a858
--- /dev/null
+++ b/hcloud/deprecation_test.go
@@ -0,0 +1,75 @@
+package hcloud
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type TestDeprecatableResource struct {
+	DeprecatableResource
+}
+
+// Interface is implemented.
+var _ Deprecatable = TestDeprecatableResource{}
+
+func TestDeprecatableResource_IsDeprecated(t *testing.T) {
+	tests := []struct {
+		name     string
+		resource TestDeprecatableResource
+		want     bool
+	}{
+		{name: "nil returns false", resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: nil}}, want: false},
+		{name: "struct returns true", resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: &DeprecationInfo{}}}, want: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, tt.resource.IsDeprecated(), "IsDeprecated()")
+		})
+	}
+}
+
+func TestDeprecatableResource_DeprecationAnnounced(t *testing.T) {
+	tests := []struct {
+		name     string
+		resource TestDeprecatableResource
+		want     time.Time
+	}{
+		{
+			name:     "nil returns default time",
+			resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: nil}},
+			want:     time.Unix(0, 0)},
+		{
+			name:     "actual value is returned",
+			resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: &DeprecationInfo{Announced: mustParseTime(t, "2023-06-01T00:00:00+00:00")}}},
+			want:     mustParseTime(t, "2023-06-01T00:00:00+00:00")},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, tt.resource.DeprecationAnnounced(), "DeprecationAnnounced()")
+		})
+	}
+}
+
+func TestDeprecatableResource_UnavailableAfter(t *testing.T) {
+	tests := []struct {
+		name     string
+		resource TestDeprecatableResource
+		want     time.Time
+	}{
+		{
+			name:     "nil returns default time",
+			resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: nil}},
+			want:     time.Unix(0, 0)},
+		{
+			name:     "actual value is returned",
+			resource: TestDeprecatableResource{DeprecatableResource: DeprecatableResource{Deprecation: &DeprecationInfo{UnavailableAfter: mustParseTime(t, "2023-06-01T00:00:00+00:00")}}},
+			want:     mustParseTime(t, "2023-06-01T00:00:00+00:00")},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, tt.resource.UnavailableAfter(), "UnavailableAfter()")
+		})
+	}
+}
diff --git a/hcloud/load_balancer_test.go b/hcloud/load_balancer_test.go
index 7d78341..1cadfc8 100644
--- a/hcloud/load_balancer_test.go
+++ b/hcloud/load_balancer_test.go
@@ -1036,14 +1036,14 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 					LoadBalancerMetricRequestsPerSecond,
 					LoadBalancerMetricBandwidth,
 				},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			respFn: func() schema.LoadBalancerGetMetricsResponse {
 				var resp schema.LoadBalancerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{
 					"open_connections": {
 						Values: []interface{}{
@@ -1080,8 +1080,8 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 				return resp
 			},
 			expected: LoadBalancerMetrics{
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 				TimeSeries: map[string][]LoadBalancerMetricsValue{
 					"open_connections": {
 						{Timestamp: 1435781470.622, Value: "42"},
@@ -1110,8 +1110,8 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 			name: "missing metrics types",
 			lb:   &LoadBalancer{ID: 3},
 			opts: LoadBalancerGetMetricsOpts{
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			expectedErr: "add query params: no metric types specified",
 		},
@@ -1120,7 +1120,7 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 			lb:   &LoadBalancer{ID: 4},
 			opts: LoadBalancerGetMetricsOpts{
 				Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth},
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			expectedErr: "add query params: no start time specified",
 		},
@@ -1129,7 +1129,7 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 			lb:   &LoadBalancer{ID: 5},
 			opts: LoadBalancerGetMetricsOpts{
 				Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 			},
 			expectedErr: "add query params: no end time specified",
 		},
@@ -1138,8 +1138,8 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 			lb:   &LoadBalancer{ID: 6},
 			opts: LoadBalancerGetMetricsOpts{
 				Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			respStatus:  http.StatusInternalServerError,
 			expectedErr: "get metrics: hcloud: server responded with status code 500",
@@ -1148,8 +1148,8 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 			name: "no load balancer passed",
 			opts: LoadBalancerGetMetricsOpts{
 				Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			expectedErr: "illegal argument: load balancer is nil",
 		},
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index e31c203..e5ac9b2 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -921,6 +921,7 @@ func TestServerTypeFromSchema(t *testing.T) {
 		"storage_type": "local",
 		"cpu_type": "shared",
 		"architecture": "x86",
+		"deprecation": null,
 		"prices": [
 			{
 				"location": "fsn1",
@@ -969,6 +970,9 @@ func TestServerTypeFromSchema(t *testing.T) {
 	if serverType.Architecture != ArchitectureX86 {
 		t.Errorf("unexpected cpu architecture: %q", serverType.Architecture)
 	}
+	if serverType.Deprecation != nil {
+		t.Errorf("unexpected deprecation: %v", serverType.Deprecation)
+	}
 	if len(serverType.Pricings) != 1 {
 		t.Errorf("unexpected number of pricings: %d", len(serverType.Pricings))
 	} else {
@@ -1948,9 +1952,9 @@ func TestCertificateFromSchema(t *testing.T) {
 				Name:           "my website cert",
 				Type:           "uploaded",
 				Certificate:    "-----BEGIN CERTIFICATE-----\n...",
-				Created:        mustParseTime(t, apiTimestampFormat, "2016-01-30T23:50:00+00:00"),
-				NotValidBefore: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:51:00+00:00"),
-				NotValidAfter:  mustParseTime(t, apiTimestampFormat, "2016-01-30T23:55:00+00:00"),
+				Created:        mustParseTime(t, "2016-01-30T23:50:00+00:00"),
+				NotValidBefore: mustParseTime(t, "2016-01-30T23:51:00+00:00"),
+				NotValidAfter:  mustParseTime(t, "2016-01-30T23:55:00+00:00"),
 				DomainNames:    []string{"example.com", "webmail.example.com", "www.example.com"},
 				Fingerprint:    "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f",
 				UsedBy: []CertificateUsedByRef{
@@ -1992,9 +1996,9 @@ func TestCertificateFromSchema(t *testing.T) {
 				Name:           "managed certificate",
 				Type:           "managed",
 				Certificate:    "-----BEGIN CERTIFICATE-----\n...",
-				Created:        mustParseTime(t, apiTimestampFormat, "2016-01-30T23:50:00+00:00"),
-				NotValidBefore: mustParseTime(t, apiTimestampFormat, "2016-01-30T23:51:00+00:00"),
-				NotValidAfter:  mustParseTime(t, apiTimestampFormat, "2016-01-30T23:55:00+00:00"),
+				Created:        mustParseTime(t, "2016-01-30T23:50:00+00:00"),
+				NotValidBefore: mustParseTime(t, "2016-01-30T23:51:00+00:00"),
+				NotValidAfter:  mustParseTime(t, "2016-01-30T23:55:00+00:00"),
 				DomainNames:    []string{"example.com", "webmail.example.com", "www.example.com"},
 				Fingerprint:    "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f",
 				Status: &CertificateStatus{
@@ -2702,8 +2706,8 @@ func TestServerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.ServerGetMetricsResponse {
 				var resp schema.ServerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{
 					"cpu": {
 						Values: []interface{}{"some value"},
@@ -2719,8 +2723,8 @@ func TestServerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.ServerGetMetricsResponse {
 				var resp schema.ServerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{
 					"cpu": {
 						Values: []interface{}{
@@ -2738,8 +2742,8 @@ func TestServerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.ServerGetMetricsResponse {
 				var resp schema.ServerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{
 					"cpu": {
 						Values: []interface{}{
@@ -2757,8 +2761,8 @@ func TestServerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.ServerGetMetricsResponse {
 				var resp schema.ServerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{
 					"cpu": {
 						Values: []interface{}{
@@ -2776,8 +2780,8 @@ func TestServerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.ServerGetMetricsResponse {
 				var resp schema.ServerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{
 					"cpu": {
 						Values: []interface{}{
@@ -2814,8 +2818,8 @@ func TestServerMetricsFromSchema(t *testing.T) {
 				return &resp
 			},
 			expected: &ServerMetrics{
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 				TimeSeries: map[string][]ServerMetricsValue{
 					"cpu": {
 						{Timestamp: 1435781470.622, Value: "42"},
@@ -2872,8 +2876,8 @@ func TestLoadBalancerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.LoadBalancerGetMetricsResponse {
 				var resp schema.LoadBalancerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{
 					"open_connections": {
 						Values: []interface{}{"some value"},
@@ -2889,8 +2893,8 @@ func TestLoadBalancerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.LoadBalancerGetMetricsResponse {
 				var resp schema.LoadBalancerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{
 					"open_connections": {
 						Values: []interface{}{
@@ -2908,8 +2912,8 @@ func TestLoadBalancerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.LoadBalancerGetMetricsResponse {
 				var resp schema.LoadBalancerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{
 					"open_connections": {
 						Values: []interface{}{
@@ -2927,8 +2931,8 @@ func TestLoadBalancerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.LoadBalancerGetMetricsResponse {
 				var resp schema.LoadBalancerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{
 					"open_connections": {
 						Values: []interface{}{
@@ -2946,8 +2950,8 @@ func TestLoadBalancerMetricsFromSchema(t *testing.T) {
 			respFn: func() *schema.LoadBalancerGetMetricsResponse {
 				var resp schema.LoadBalancerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.LoadBalancerTimeSeriesVals{
 					"open_connections": {
 						Values: []interface{}{
@@ -2984,8 +2988,8 @@ func TestLoadBalancerMetricsFromSchema(t *testing.T) {
 				return &resp
 			},
 			expected: &LoadBalancerMetrics{
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 				TimeSeries: map[string][]LoadBalancerMetricsValue{
 					"open_connections": {
 						{Timestamp: 1435781470.622, Value: "42"},
@@ -3169,3 +3173,43 @@ func TestPlacementGroupFromSchema(t *testing.T) {
 		t.Errorf("unexpected Type %s", placementGroup.Type)
 	}
 }
+
+func TestDeprecationFromSchema(t *testing.T) {
+	t.Run("Deprecated Resource", func(t *testing.T) {
+		data := []byte(`{
+			"deprecation": {
+				"announced": "2023-06-01T00:00:00+00:00",
+				"unavailable_after": "2023-09-01T00:00:00+00:00"
+			}
+		}`)
+
+		var d schema.DeprecatableResource
+		if err := json.Unmarshal(data, &d); err != nil {
+			t.Fatal(err)
+		}
+		deprecationInfo := DeprecationFromSchema(d.Deprecation)
+		if deprecationInfo == nil {
+			t.Fatal("unexpected nil DeprecationInfo")
+		}
+		if deprecationInfo.Announced != mustParseTime(t, "2023-06-01T00:00:00+00:00") {
+			t.Errorf("unexpected anounce time %s", deprecationInfo.Announced)
+		}
+		if deprecationInfo.UnavailableAfter != mustParseTime(t, "2023-09-01T00:00:00+00:00") {
+			t.Errorf("unexpected unavailable after time %s", deprecationInfo.UnavailableAfter)
+		}
+	})
+
+	t.Run("Not-deprecated Resource", func(t *testing.T) {
+		data := []byte(`{
+			"deprecation": null
+		}`)
+		var d schema.DeprecatableResource
+		if err := json.Unmarshal(data, &d); err != nil {
+			t.Fatal(err)
+		}
+		deprecationInfo := DeprecationFromSchema(d.Deprecation)
+		if deprecationInfo != nil {
+			t.Fatal("unexpected non-nil DeprecationInfo", deprecationInfo)
+		}
+	})
+}
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index cb1b821..0207a55 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -2122,14 +2122,14 @@ func TestServerGetMetrics(t *testing.T) {
 			server: &Server{ID: 1},
 			opts: ServerGetMetricsOpts{
 				Types: []ServerMetricType{ServerMetricCPU},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			respFn: func() schema.ServerGetMetricsResponse {
 				var resp schema.ServerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{
 					"cpu": {
 						Values: []interface{}{
@@ -2142,8 +2142,8 @@ func TestServerGetMetrics(t *testing.T) {
 				return resp
 			},
 			expected: ServerMetrics{
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 				TimeSeries: map[string][]ServerMetricsValue{
 					"cpu": {
 						{Timestamp: 1435781470.622, Value: "42"},
@@ -2161,14 +2161,14 @@ func TestServerGetMetrics(t *testing.T) {
 					ServerMetricDisk,
 					ServerMetricNetwork,
 				},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			respFn: func() schema.ServerGetMetricsResponse {
 				var resp schema.ServerGetMetricsResponse
 
-				resp.Metrics.Start = mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z")
-				resp.Metrics.End = mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z")
+				resp.Metrics.Start = mustParseTime(t, "2017-01-01T00:00:00Z")
+				resp.Metrics.End = mustParseTime(t, "2017-01-01T23:00:00Z")
 				resp.Metrics.TimeSeries = map[string]schema.ServerTimeSeriesVals{
 					"cpu": {
 						Values: []interface{}{
@@ -2205,8 +2205,8 @@ func TestServerGetMetrics(t *testing.T) {
 				return resp
 			},
 			expected: ServerMetrics{
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 				TimeSeries: map[string][]ServerMetricsValue{
 					"cpu": {
 						{Timestamp: 1435781470.622, Value: "42"},
@@ -2235,8 +2235,8 @@ func TestServerGetMetrics(t *testing.T) {
 			name:   "missing metrics types",
 			server: &Server{ID: 3},
 			opts: ServerGetMetricsOpts{
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			expectedErr: "add query params: no metric types specified",
 		},
@@ -2245,7 +2245,7 @@ func TestServerGetMetrics(t *testing.T) {
 			server: &Server{ID: 4},
 			opts: ServerGetMetricsOpts{
 				Types: []ServerMetricType{ServerMetricCPU},
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			expectedErr: "add query params: no start time specified",
 		},
@@ -2254,7 +2254,7 @@ func TestServerGetMetrics(t *testing.T) {
 			server: &Server{ID: 5},
 			opts: ServerGetMetricsOpts{
 				Types: []ServerMetricType{ServerMetricCPU},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 			},
 			expectedErr: "add query params: no end time specified",
 		},
@@ -2263,8 +2263,8 @@ func TestServerGetMetrics(t *testing.T) {
 			server: &Server{ID: 6},
 			opts: ServerGetMetricsOpts{
 				Types: []ServerMetricType{ServerMetricCPU},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			respStatus:  http.StatusInternalServerError,
 			expectedErr: "get metrics: hcloud: server responded with status code 500",
@@ -2273,8 +2273,8 @@ func TestServerGetMetrics(t *testing.T) {
 			name: "no server passed",
 			opts: ServerGetMetricsOpts{
 				Types: []ServerMetricType{ServerMetricCPU},
-				Start: mustParseTime(t, time.RFC3339, "2017-01-01T00:00:00Z"),
-				End:   mustParseTime(t, time.RFC3339, "2017-01-01T23:00:00Z"),
+				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
+				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
 			expectedErr: "illegal argument: server is nil",
 		},
diff --git a/hcloud/testing.go b/hcloud/testing.go
index a3cfb2f..63cd92f 100644
--- a/hcloud/testing.go
+++ b/hcloud/testing.go
@@ -5,14 +5,12 @@ import (
 	"time"
 )
 
-const apiTimestampFormat = "2006-01-02T15:04:05-07:00"
-
-func mustParseTime(t *testing.T, layout, value string) time.Time {
+func mustParseTime(t *testing.T, value string) time.Time {
 	t.Helper()
 
-	ts, err := time.Parse(layout, value)
+	ts, err := time.Parse(time.RFC3339, value)
 	if err != nil {
-		t.Fatalf("parse time: layout %v: value %v: %v", layout, value, err)
+		t.Fatalf("parse time: value %v: %v", value, err)
 	}
 	return ts
 }