Skip to content

Commit 0a30c87

Browse files
jooolaapricotelukasmetznerphm07
authored
feat: per location server types (#1166)
[Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) now depend on [Locations](https://docs.hetzner.cloud/reference/cloud#locations). - We added a new `locations` property to the [Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) resource. The new property defines a list of supported [Locations](https://docs.hetzner.cloud/reference/cloud#locations) and additional per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) details such as deprecations information. - We deprecated the `deprecation` property from the [Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) resource. The property will gradually be phased out as per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) deprecations are being announced. Please use the new per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) deprecation information instead. See our [changelog](https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types) for more details. Co-authored-by: Julian Tölle <julian.toelle@hetzner-cloud.de> Co-authored-by: Lukas Metzner <lukas.metzner@hetzner-cloud.de> Co-authored-by: phm07 <22707808+phm07@users.noreply.github.com>
1 parent 50897df commit 0a30c87

11 files changed

Lines changed: 189 additions & 62 deletions

File tree

internal/cmd/server/change_type.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ var ChangeTypeCmd = base.Cmd{
4747
return fmt.Errorf("Server Type not found: %s", serverTypeIDOrName)
4848
}
4949

50-
if serverType.IsDeprecated() {
51-
cmd.Print(warningDeprecatedServerType(serverType))
52-
}
50+
cmd.Print(deprecatedServerTypeWarning(serverType, server.Datacenter.Location.Name))
5351

5452
keepDisk, _ := cmd.Flags().GetBool("keep-disk")
5553
opts := hcloud.ServerChangeTypeOpts{

internal/cmd/server/change_type_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ func TestChangeType(t *testing.T) {
1919
cmd := server.ChangeTypeCmd.CobraCommand(fx.State())
2020
fx.ExpectEnsureToken()
2121

22-
srv := &hcloud.Server{ID: 123, Name: "my-server"}
23-
st := &hcloud.ServerType{ID: 456, Name: "cax21"}
22+
srv := &hcloud.Server{ID: 123, Name: "my-server", Datacenter: &hcloud.Datacenter{Location: &hcloud.Location{Name: "fsn1"}}}
23+
st := &hcloud.ServerType{ID: 456, Name: "cax21", Locations: []hcloud.ServerTypeLocation{{Location: &hcloud.Location{Name: "fsn1"}}}}
2424

2525
fx.Client.ServerClient.EXPECT().
2626
Get(gomock.Any(), "my-server").
@@ -53,8 +53,8 @@ func TestChangeTypeKeepDisk(t *testing.T) {
5353
cmd := server.ChangeTypeCmd.CobraCommand(fx.State())
5454
fx.ExpectEnsureToken()
5555

56-
srv := &hcloud.Server{ID: 123, Name: "my-server"}
57-
st := &hcloud.ServerType{ID: 456, Name: "cax21"}
56+
srv := &hcloud.Server{ID: 123, Name: "my-server", Datacenter: &hcloud.Datacenter{Location: &hcloud.Location{Name: "fsn1"}}}
57+
st := &hcloud.ServerType{ID: 456, Name: "cax21", Locations: []hcloud.ServerTypeLocation{{Location: &hcloud.Location{Name: "fsn1"}}}}
5858

5959
fx.Client.ServerClient.EXPECT().
6060
Get(gomock.Any(), "my-server").

internal/cmd/server/create.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ var CreateCmd = base.CreateCmd[*createResult]{
105105
return nil, nil, err
106106
}
107107

108+
// Check if intended server type is deprecated in the requested location
109+
var locName string
110+
if createOpts.Location != nil {
111+
locName = createOpts.Location.Name
112+
} else if createOpts.Datacenter != nil {
113+
locName = createOpts.Datacenter.Location.Name
114+
}
115+
116+
cmd.Print(deprecatedServerTypeWarning(createOpts.ServerType, locName))
117+
108118
result, _, err := s.Client().Server().Create(s, createOpts)
109119
if err != nil {
110120
return nil, nil, err
@@ -244,8 +254,8 @@ func createOptsFromFlags(
244254
name, _ := flags.GetString("name")
245255
serverTypeName, _ := flags.GetString("type")
246256
imageIDorName, _ := flags.GetString("image")
247-
location, _ := flags.GetString("location")
248-
datacenter, _ := flags.GetString("datacenter")
257+
locationIDOrName, _ := flags.GetString("location")
258+
datacenterIDOrName, _ := flags.GetString("datacenter")
249259
userDataFiles, _ := flags.GetStringArray("user-data-from-file")
250260
startAfterCreate, _ := flags.GetBool("start-after-create")
251261
sshKeys, _ := flags.GetStringSlice("ssh-key")
@@ -271,10 +281,6 @@ func createOptsFromFlags(
271281
return
272282
}
273283

274-
if serverType.IsDeprecated() {
275-
cmd.Print(warningDeprecatedServerType(serverType))
276-
}
277-
278284
// Select correct image based on Server Type architecture
279285
image, _, err := s.Client().Image().GetForArchitecture(s, imageIDorName, serverType.Architecture)
280286
if err != nil {
@@ -424,12 +430,31 @@ func createOptsFromFlags(
424430
createOpts.Firewalls = append(createOpts.Firewalls, &hcloud.ServerCreateFirewall{Firewall: *firewall})
425431
}
426432

427-
if datacenter != "" {
428-
createOpts.Datacenter = &hcloud.Datacenter{Name: datacenter}
433+
if datacenterIDOrName != "" {
434+
var datacenter *hcloud.Datacenter
435+
datacenter, _, err = s.Client().Datacenter().Get(s, datacenterIDOrName)
436+
if err != nil {
437+
return
438+
}
439+
if datacenter == nil {
440+
err = fmt.Errorf("Datacenter not found: %s", datacenterIDOrName)
441+
return
442+
}
443+
createOpts.Datacenter = datacenter
429444
}
430-
if location != "" {
431-
createOpts.Location = &hcloud.Location{Name: location}
445+
if locationIDOrName != "" {
446+
var location *hcloud.Location
447+
location, _, err = s.Client().Location().Get(s, locationIDOrName)
448+
if err != nil {
449+
return
450+
}
451+
if location == nil {
452+
err = fmt.Errorf("Location not found: %s", locationIDOrName)
453+
return
454+
}
455+
createOpts.Location = location
432456
}
457+
433458
if placementGroupIDorName != "" {
434459
var placementGroup *hcloud.PlacementGroup
435460
placementGroup, _, err = s.Client().PlacementGroup().Get(s, placementGroupIDorName)

internal/cmd/server/create_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestCreate(t *testing.T) {
3030

3131
fx.Client.ServerTypeClient.EXPECT().
3232
Get(gomock.Any(), "cx22").
33-
Return(&hcloud.ServerType{Architecture: hcloud.ArchitectureX86}, nil, nil)
33+
Return(&hcloud.ServerType{Architecture: hcloud.ArchitectureX86, Locations: []hcloud.ServerTypeLocation{{Location: &hcloud.Location{Name: "fsn1"}}}}, nil, nil)
3434
fx.Client.ImageClient.EXPECT().
3535
GetForArchitecture(gomock.Any(), "ubuntu-20.04", hcloud.ArchitectureX86).
3636
Return(&hcloud.Image{}, nil, nil)
@@ -193,7 +193,7 @@ func TestCreateJSON(t *testing.T) {
193193

194194
fx.Client.ServerTypeClient.EXPECT().
195195
Get(gomock.Any(), "cx22").
196-
Return(&hcloud.ServerType{Architecture: hcloud.ArchitectureX86}, nil, nil)
196+
Return(&hcloud.ServerType{Architecture: hcloud.ArchitectureX86, Locations: []hcloud.ServerTypeLocation{{Location: &hcloud.Location{Name: "fsn1"}}}}, nil, nil)
197197
fx.Client.ImageClient.EXPECT().
198198
GetForArchitecture(gomock.Any(), "ubuntu-20.04", hcloud.ArchitectureX86).
199199
Return(&hcloud.Image{}, nil, nil)
@@ -236,7 +236,7 @@ func TestCreateProtectionBackup(t *testing.T) {
236236

237237
fx.Client.ServerTypeClient.EXPECT().
238238
Get(gomock.Any(), "cx22").
239-
Return(&hcloud.ServerType{Architecture: hcloud.ArchitectureX86}, nil, nil)
239+
Return(&hcloud.ServerType{Architecture: hcloud.ArchitectureX86, Locations: []hcloud.ServerTypeLocation{{Location: &hcloud.Location{Name: "fsn1"}}}}, nil, nil)
240240
fx.Client.ImageClient.EXPECT().
241241
GetForArchitecture(gomock.Any(), "ubuntu-20.04", hcloud.ArchitectureX86).
242242
Return(&hcloud.Image{}, nil, nil)

internal/cmd/server/describe.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"fmt"
5+
"slices"
56

67
humanize "github.com/dustin/go-humanize"
78
"github.com/spf13/cobra"
@@ -40,8 +41,15 @@ var DescribeCmd = base.DescribeCmd[*hcloud.Server]{
4041
cmd.Printf(" Disk:\t\t%d GB\n", server.PrimaryDiskSize)
4142
cmd.Printf(" Storage Type:\t%s\n", server.ServerType.StorageType)
4243

43-
if text := util.DescribeDeprecation(server.ServerType); text != "" {
44-
cmd.Print(util.PrefixLines(text, " "))
44+
// As we already know the location the server is in, we can show the deprecation info
45+
// of that server type in that specific location.
46+
locationInfoIndex := slices.IndexFunc(server.ServerType.Locations, func(locInfo hcloud.ServerTypeLocation) bool {
47+
return locInfo.Location.Name == server.Datacenter.Location.Name
48+
})
49+
if locationInfoIndex >= 0 {
50+
if text := util.DescribeDeprecation(server.ServerType.Locations[locationInfoIndex]); text != "" {
51+
cmd.Print(util.PrefixLines(text, " "))
52+
}
4553
}
4654

4755
cmd.Printf("Public Net:\n")

internal/cmd/server/describe_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ func TestDescribe(t *testing.T) {
2525
cmd := server.DescribeCmd.CobraCommand(fx.State())
2626
fx.ExpectEnsureToken()
2727

28+
serverTypeDeprecation := hcloud.DeprecatableResource{Deprecation: &hcloud.DeprecationInfo{
29+
Announced: time.Date(2036, 1, 1, 0, 0, 0, 0, time.UTC),
30+
UnavailableAfter: time.Date(2036, 4, 1, 0, 0, 0, 0, time.UTC),
31+
}}
32+
2833
srv := &hcloud.Server{
2934
ID: 123,
3035
Name: "test",
@@ -37,6 +42,15 @@ func TestDescribe(t *testing.T) {
3742
Memory: 4.0,
3843
Disk: 40,
3944
StorageType: hcloud.StorageTypeLocal,
45+
Locations: []hcloud.ServerTypeLocation{
46+
{
47+
Location: &hcloud.Location{Name: "fsn1"},
48+
},
49+
{
50+
Location: &hcloud.Location{Name: "hel1"},
51+
DeprecatableResource: serverTypeDeprecation,
52+
},
53+
},
4054
},
4155
Image: &hcloud.Image{
4256
ID: 123,
@@ -91,6 +105,9 @@ Server Type: cax11 (ID: 45)
91105
Memory: 4 GB
92106
Disk: 0 GB
93107
Storage Type: local
108+
Deprecation:
109+
Announced: %s (%s)
110+
Unavailable After: %s (%s)
94111
Public Net:
95112
IPv4:
96113
No Primary IPv4
@@ -142,7 +159,10 @@ Placement Group:
142159
No Placement Group set
143160
`,
144161
util.Datetime(srv.Created), humanize.Time(srv.Created),
145-
util.Datetime(srv.Image.Created), humanize.Time(srv.Image.Created))
162+
util.Datetime(serverTypeDeprecation.DeprecationAnnounced()), humanize.Time(serverTypeDeprecation.DeprecationAnnounced()),
163+
util.Datetime(serverTypeDeprecation.UnavailableAfter()), humanize.Time(serverTypeDeprecation.UnavailableAfter()),
164+
util.Datetime(srv.Image.Created), humanize.Time(srv.Image.Created),
165+
)
146166

147167
require.NoError(t, err)
148168
assert.Empty(t, errOut)

internal/cmd/server/texts.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@ package server
22

33
import (
44
"fmt"
5-
"time"
65

76
"github.com/hetznercloud/hcloud-go/v2/hcloud"
7+
"github.com/hetznercloud/hcloud-go/v2/hcloud/exp/deprecationutil"
88
)
99

10-
func warningDeprecatedServerType(serverType *hcloud.ServerType) string {
11-
if !serverType.IsDeprecated() {
12-
return ""
13-
}
10+
const ChangeDeprecatedServerTypeMessage = (`Existing servers of that plan will ` +
11+
`continue to work as before and no action is required on your part. ` +
12+
`It is possible to migrate this Server to another Server Type by using ` +
13+
`the "hcloud server change-type" command.`)
1414

15-
if time.Now().After(serverType.UnavailableAfter()) {
16-
return fmt.Sprintf("Attention: The Server Type %q is deprecated and can no longer be ordered. Existing servers of that plan will continue to work as before and no action is required on your part. It is possible to migrate this Server to another Server Type by using the \"hcloud server change-type\" command.\n\n", serverType.Name)
15+
func deprecatedServerTypeWarning(serverType *hcloud.ServerType, locationName string) string {
16+
warnText, _ := deprecationutil.ServerTypeWarning(serverType, locationName)
17+
if warnText == "" {
18+
return ""
1719
}
1820

19-
return fmt.Sprintf("Attention: The Server Type %q is deprecated and will no longer be available for order as of %s. Existing servers of that plan will continue to work as before and no action is required on your part. It is possible to migrate this Server to another Server Type by using the \"hcloud server change-type\" command.\n\n", serverType.Name, serverType.UnavailableAfter().Format(time.DateOnly))
21+
return fmt.Sprintf("Attention: %s %s\n\n", warnText, ChangeDeprecatedServerTypeMessage)
2022
}

internal/cmd/servertype/describe.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,28 @@ var DescribeCmd = base.DescribeCmd[*hcloud.ServerType]{
3333
cmd.Printf("Memory:\t\t\t%.1f GB\n", serverType.Memory)
3434
cmd.Printf("Disk:\t\t\t%d GB\n", serverType.Disk)
3535
cmd.Printf("Storage Type:\t\t%s\n", serverType.StorageType)
36-
cmd.Print(util.DescribeDeprecation(serverType))
3736

3837
pricings, err := fullPricingInfo(s, serverType)
3938
if err != nil {
4039
cmd.PrintErrf("failed to get prices for Server Type: %v", err)
4140
}
4241

43-
if pricings != nil {
44-
cmd.Printf("Pricings per Location:\n")
45-
for _, price := range pricings {
46-
cmd.Printf(" - Location:\t\t%s\n", price.Location.Name)
47-
cmd.Printf(" Hourly:\t\t%s\n", util.GrossPrice(price.Hourly))
48-
cmd.Printf(" Monthly:\t\t%s\n", util.GrossPrice(price.Monthly))
49-
cmd.Printf(" Included Traffic:\t%s\n", humanize.IBytes(price.IncludedTraffic))
50-
cmd.Printf(" Additional Traffic:\t%s per TB\n", util.GrossPrice(price.PerTBTraffic))
51-
cmd.Printf("\n")
42+
locations := joinLocationInfo(serverType, pricings)
43+
cmd.Printf("Locations:\n")
44+
for _, info := range locations {
45+
46+
cmd.Printf(" - Location:\t\t%s\n", info.Location.Name)
47+
48+
if deprecationText := util.DescribeDeprecation(info); deprecationText != "" {
49+
cmd.Print(util.PrefixLines(deprecationText, " "))
5250
}
51+
52+
cmd.Printf(" Pricing:\n")
53+
cmd.Printf(" Hourly:\t\t%s\n", util.GrossPrice(info.Pricing.Hourly))
54+
cmd.Printf(" Monthly:\t\t%s\n", util.GrossPrice(info.Pricing.Monthly))
55+
cmd.Printf(" Included Traffic:\t%s\n", humanize.IBytes(info.Pricing.IncludedTraffic))
56+
cmd.Printf(" Additional Traffic:\t%s per TB\n", util.GrossPrice(info.Pricing.PerTBTraffic))
57+
cmd.Printf("\n")
5358
}
5459

5560
return nil
@@ -70,3 +75,29 @@ func fullPricingInfo(s state.State, serverType *hcloud.ServerType) ([]hcloud.Ser
7075

7176
return nil, nil
7277
}
78+
79+
type locationInfo struct {
80+
Location *hcloud.Location
81+
hcloud.DeprecatableResource
82+
Pricing hcloud.ServerTypeLocationPricing
83+
}
84+
85+
func joinLocationInfo(serverType *hcloud.ServerType, pricings []hcloud.ServerTypeLocationPricing) []locationInfo {
86+
locations := make([]locationInfo, 0, len(serverType.Locations))
87+
88+
for _, location := range serverType.Locations {
89+
info := locationInfo{Location: location.Location, DeprecatableResource: location.DeprecatableResource}
90+
91+
for _, pricing := range pricings {
92+
// Pricing endpoint only sets the location name
93+
if pricing.Location.Name == info.Location.Name {
94+
info.Pricing = pricing
95+
break
96+
}
97+
}
98+
99+
locations = append(locations, info)
100+
}
101+
102+
return locations
103+
}

0 commit comments

Comments
 (0)