Skip to content

Commit

Permalink
feat(servertype): implement new Deprecation api field (hetznercloud#268)
Browse files Browse the repository at this point in the history
* 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.

* refactor(rdns): make sure the all expected resources comply with interface

* test: fix linting issue

Argument layout of mustParseTime was
always set to the same value.
  • Loading branch information
apricote authored Jun 13, 2023
1 parent f396b84 commit ac5ae2e
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 71 deletions.
59 changes: 59 additions & 0 deletions hcloud/deprecation.go
Original file line number Diff line number Diff line change
@@ -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{}
75 changes: 75 additions & 0 deletions hcloud/deprecation_test.go
Original file line number Diff line number Diff line change
@@ -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()")
})
}
}
28 changes: 14 additions & 14 deletions hcloud/load_balancer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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",
},
Expand All @@ -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",
},
Expand All @@ -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",
},
Expand All @@ -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",
Expand All @@ -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",
},
Expand Down
10 changes: 8 additions & 2 deletions hcloud/rdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand All @@ -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{}
16 changes: 16 additions & 0 deletions hcloud/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -302,6 +305,7 @@ func ServerTypeFromSchema(s schema.ServerType) *ServerType {
},
})
}

return st
}

Expand Down Expand Up @@ -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,
}
}
12 changes: 12 additions & 0 deletions hcloud/schema/deprecation.go
Original file line number Diff line number Diff line change
@@ -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"`
}
1 change: 1 addition & 0 deletions hcloud/schema/server_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit ac5ae2e

Please sign in to comment.