diff --git a/docs/reference/manual/hcloud_primary-ip_create.md b/docs/reference/manual/hcloud_primary-ip_create.md index 61ee63f0..a9d90815 100644 --- a/docs/reference/manual/hcloud_primary-ip_create.md +++ b/docs/reference/manual/hcloud_primary-ip_create.md @@ -2,6 +2,13 @@ Create a Primary IP +### Synopsis + +Create a Primary IP. + +The --datacenter flag is deprecated. Use --location or --assignee-id instead. +See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters. + ``` hcloud primary-ip create [options] --type --name ``` @@ -9,12 +16,13 @@ hcloud primary-ip create [options] --type --name ### Options ``` - --assignee-id int Assignee (usually a Server) to assign Primary IP to (required if 'datacenter' is not specified) + --assignee-id int Assignee (usually a Server) to assign Primary IP to --auto-delete Delete Primary IP if assigned resource is deleted (true, false) - --datacenter string Datacenter (ID or name) (required if 'assignee-id' is not specified) + --datacenter string Datacenter (name) (deprecated) --enable-protection strings Enable protection (delete) (default: none) -h, --help help for create --label stringToString User-defined labels ('key=value') (can be specified multiple times) (default []) + --location string Location (ID or name) of Primary IP --name string Name (required) -o, --output stringArray output options: json|yaml --type string Type (ipv4 or ipv6) (required) diff --git a/docs/reference/manual/hcloud_server_create.md b/docs/reference/manual/hcloud_server_create.md index 6ab4f586..bc10aa8f 100644 --- a/docs/reference/manual/hcloud_server_create.md +++ b/docs/reference/manual/hcloud_server_create.md @@ -2,6 +2,13 @@ Create a Server +### Synopsis + +Create a Server. + +The --datacenter flag is deprecated. Use --location instead. +See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters. + ``` hcloud server create [options] --name --type --image ``` @@ -11,7 +18,7 @@ hcloud server create [options] --name --type --image --name ", - Short: "Create a Primary IP", + Use: "create [options] --type --name ", + Short: "Create a Primary IP", + Long: `Create a Primary IP. + +The --datacenter flag is deprecated. Use --location or --assignee-id instead. +See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.`, TraverseChildren: true, DisableFlagsInUseLine: true, } @@ -28,9 +33,12 @@ var CreateCmd = base.CreateCmd[*hcloud.PrimaryIP]{ cmd.Flags().String("name", "", "Name (required)") _ = cmd.MarkFlagRequired("name") - cmd.Flags().Int64("assignee-id", 0, "Assignee (usually a Server) to assign Primary IP to (required if 'datacenter' is not specified)") + cmd.Flags().Int64("assignee-id", 0, "Assignee (usually a Server) to assign Primary IP to") + + cmd.Flags().String("location", "", "Location (ID or name) of Primary IP") + _ = cmd.RegisterFlagCompletionFunc("location", cmpl.SuggestCandidatesF(client.Location().Names)) - cmd.Flags().String("datacenter", "", "Datacenter (ID or name) (required if 'assignee-id' is not specified)") + cmd.Flags().String("datacenter", "", "Datacenter (name) (deprecated)") _ = cmd.RegisterFlagCompletionFunc("datacenter", cmpl.SuggestCandidatesF(client.Datacenter().Names)) cmd.Flags().StringToString("label", nil, "User-defined labels ('key=value') (can be specified multiple times)") @@ -40,8 +48,8 @@ var CreateCmd = base.CreateCmd[*hcloud.PrimaryIP]{ cmd.Flags().Bool("auto-delete", false, "Delete Primary IP if assigned resource is deleted (true, false)") - cmd.MarkFlagsOneRequired("assignee-id", "datacenter") - cmd.MarkFlagsMutuallyExclusive("assignee-id", "datacenter") + cmd.MarkFlagsOneRequired("assignee-id", "datacenter", "location") + cmd.MarkFlagsMutuallyExclusive("assignee-id", "datacenter", "location") return cmd }, Run: func(s state.State, cmd *cobra.Command, _ []string) (*hcloud.PrimaryIP, any, error) { @@ -49,6 +57,7 @@ var CreateCmd = base.CreateCmd[*hcloud.PrimaryIP]{ name, _ := cmd.Flags().GetString("name") assigneeID, _ := cmd.Flags().GetInt64("assignee-id") datacenter, _ := cmd.Flags().GetString("datacenter") + locationIDOrName, _ := cmd.Flags().GetString("location") labels, _ := cmd.Flags().GetStringToString("label") protection, _ := cmd.Flags().GetStringSlice("enable-protection") autoDelete, _ := cmd.Flags().GetBool("auto-delete") @@ -62,7 +71,6 @@ var CreateCmd = base.CreateCmd[*hcloud.PrimaryIP]{ Type: hcloud.PrimaryIPType(typ), Name: name, AssigneeType: "server", - Datacenter: datacenter, Labels: labels, } if assigneeID != 0 { @@ -71,6 +79,30 @@ var CreateCmd = base.CreateCmd[*hcloud.PrimaryIP]{ if cmd.Flags().Changed("auto-delete") { createOpts.AutoDelete = &autoDelete } + if cmd.Flags().Changed("location") { + location, _, err := s.Client().Location().Get(s, locationIDOrName) + if err != nil { + return nil, nil, err + } + if location == nil { + return nil, nil, fmt.Errorf("Location not found: %s", locationIDOrName) + } + createOpts.Location = location.Name + } + if cmd.Flags().Changed("datacenter") { + cmd.PrintErrln("Warning: The --datacenter flag is deprecated. Use --location or --assignee-id instead.") + + // Backward compatible datacenter argument. + // datacenter hel1-dc2 => location hel1 + parts := strings.Split(datacenter, "-") + + if len(parts) != 2 { + return nil, nil, fmt.Errorf("Datacenter name is not valid, expected format $LOCATION-$DATACENTER, but got: %s", datacenter) + } + + locationName := parts[0] + createOpts.Location = locationName + } result, _, err := s.Client().PrimaryIP().Create(s, createOpts) if err != nil { diff --git a/internal/cmd/primaryip/create_test.go b/internal/cmd/primaryip/create_test.go index 84937717..2dff3cfc 100644 --- a/internal/cmd/primaryip/create_test.go +++ b/internal/cmd/primaryip/create_test.go @@ -39,7 +39,7 @@ func TestCreate(t *testing.T) { hcloud.PrimaryIPCreateOpts{ Name: "my-ip", Type: "ipv4", - Datacenter: "fsn1-dc14", + Location: "fsn1", Labels: map[string]string{"foo": "bar"}, AssigneeType: "server", AutoDelete: hcloud.Ptr(true), @@ -62,12 +62,14 @@ func TestCreate(t *testing.T) { out, errOut, err := fx.Run(cmd, []string{"--name=my-ip", "--type=ipv4", "--datacenter=fsn1-dc14", "--auto-delete", "--label", "foo=bar"}) + expErr := "Warning: The --datacenter flag is deprecated. Use --location or --assignee-id instead.\n" + expOut := `Primary IP 1 created IPv4: 192.168.2.1 ` require.NoError(t, err) - assert.Empty(t, errOut) + assert.Equal(t, expErr, errOut) assert.Equal(t, expOut, out) } @@ -85,6 +87,9 @@ func TestCreateJSON(t *testing.T) { Name: "my-ip", IP: net.ParseIP("192.168.2.1"), Type: "ipv4", + Location: &hcloud.Location{ + Name: "fsn1", + }, Datacenter: &hcloud.Datacenter{ ID: 1, Name: "fsn1-dc14", @@ -104,7 +109,7 @@ func TestCreateJSON(t *testing.T) { hcloud.PrimaryIPCreateOpts{ Name: "my-ip", Type: "ipv4", - Datacenter: "fsn1-dc14", + Location: "fsn1", Labels: map[string]string{"foo": "bar"}, AssigneeType: "server", AutoDelete: hcloud.Ptr(true), @@ -124,7 +129,9 @@ func TestCreateJSON(t *testing.T) { jsonOut, out, err := fx.Run(cmd, []string{"-o=json", "--name=my-ip", "--type=ipv4", "--datacenter=fsn1-dc14", "--auto-delete", "--label", "foo=bar"}) - expOut := "Primary IP 1 created\n" + expOut := `Warning: The --datacenter flag is deprecated. Use --location or --assignee-id instead. +Primary IP 1 created +` require.NoError(t, err) assert.Equal(t, expOut, out) diff --git a/internal/cmd/primaryip/describe.go b/internal/cmd/primaryip/describe.go index 94d30a69..f78e31de 100644 --- a/internal/cmd/primaryip/describe.go +++ b/internal/cmd/primaryip/describe.go @@ -9,6 +9,7 @@ import ( "github.com/hetznercloud/cli/internal/cmd/base" "github.com/hetznercloud/cli/internal/cmd/datacenter" + "github.com/hetznercloud/cli/internal/cmd/location" "github.com/hetznercloud/cli/internal/cmd/util" "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/cli/internal/state" @@ -62,8 +63,15 @@ var DescribeCmd = base.DescribeCmd[*hcloud.PrimaryIP]{ util.DescribeLabels(out, primaryIP.Labels, "") fmt.Fprintln(out) - fmt.Fprintf(out, "Datacenter:\n") - fmt.Fprintf(out, "%s", util.PrefixLines(datacenter.DescribeDatacenter(s.Client(), primaryIP.Datacenter, true), " ")) + fmt.Fprintf(out, "Location:\n") + fmt.Fprintf(out, "%s", util.PrefixLines(location.DescribeLocation(primaryIP.Location), " ")) + + if primaryIP.Datacenter != nil { + fmt.Fprintln(out) + fmt.Fprintf(out, "Datacenter:\n") + fmt.Fprintf(out, "%s", util.PrefixLines(datacenter.DescribeDatacenter(s.Client(), primaryIP.Datacenter, true), " ")) + } + return nil }, } diff --git a/internal/cmd/primaryip/describe_test.go b/internal/cmd/primaryip/describe_test.go index 9d9a8c08..5263a101 100644 --- a/internal/cmd/primaryip/describe_test.go +++ b/internal/cmd/primaryip/describe_test.go @@ -35,7 +35,16 @@ func TestDescribe(t *testing.T) { Blocked: true, AutoDelete: false, AssigneeType: "server", - Datacenter: &hcloud.Datacenter{ID: 0, Location: &hcloud.Location{ID: 0}}, + Location: &hcloud.Location{ + ID: 3, + Name: "hel1", + Description: "Helsinki DC Park 1", + NetworkZone: "eu-central", + Country: "FI", + City: "Helsinki", + Latitude: 60.169855, + Longitude: 24.938379, + }, } fx.Client.PrimaryIPClient.EXPECT(). @@ -64,20 +73,15 @@ Protection: Labels: No labels -Datacenter: - ID: 0 - Name: - Description: - - Location: - ID: 0 - Name: - Description: - Network Zone: - Country: - City: - Latitude: 0.000000 - Longitude: 0.000000 +Location: + ID: 3 + Name: hel1 + Description: Helsinki DC Park 1 + Network Zone: eu-central + Country: FI + City: Helsinki + Latitude: 60.169855 + Longitude: 24.938379 `, util.Datetime(primaryIP.Created), humanize.Time(primaryIP.Created)) require.NoError(t, err) diff --git a/internal/cmd/primaryip/testdata/create_response.json b/internal/cmd/primaryip/testdata/create_response.json index 6fa10ef2..9633d15e 100644 --- a/internal/cmd/primaryip/testdata/create_response.json +++ b/internal/cmd/primaryip/testdata/create_response.json @@ -12,7 +12,7 @@ "id": 0, "latitude": 0, "longitude": 0, - "name": "", + "name": "fsn1", "network_zone": "" }, "datacenter": { diff --git a/internal/cmd/server/change_type.go b/internal/cmd/server/change_type.go index f1e5ec64..95afead4 100644 --- a/internal/cmd/server/change_type.go +++ b/internal/cmd/server/change_type.go @@ -47,7 +47,7 @@ var ChangeTypeCmd = base.Cmd{ return fmt.Errorf("Server Type not found: %s", serverTypeIDOrName) } - cmd.Print(deprecatedServerTypeWarning(serverType, server.Datacenter.Location.Name)) + cmd.Print(deprecatedServerTypeWarning(serverType, server.Location.Name)) keepDisk, _ := cmd.Flags().GetBool("keep-disk") opts := hcloud.ServerChangeTypeOpts{ diff --git a/internal/cmd/server/change_type_test.go b/internal/cmd/server/change_type_test.go index 64cd8133..f7c2b566 100644 --- a/internal/cmd/server/change_type_test.go +++ b/internal/cmd/server/change_type_test.go @@ -19,7 +19,7 @@ func TestChangeType(t *testing.T) { cmd := server.ChangeTypeCmd.CobraCommand(fx.State()) fx.ExpectEnsureToken() - srv := &hcloud.Server{ID: 123, Name: "my-server", Datacenter: &hcloud.Datacenter{Location: &hcloud.Location{Name: "fsn1"}}} + srv := &hcloud.Server{ID: 123, Name: "my-server", Location: &hcloud.Location{Name: "fsn1"}} st := &hcloud.ServerType{ID: 456, Name: "cax21", Locations: []hcloud.ServerTypeLocation{{Location: &hcloud.Location{Name: "fsn1"}}}} fx.Client.ServerClient.EXPECT(). @@ -53,7 +53,7 @@ func TestChangeTypeKeepDisk(t *testing.T) { cmd := server.ChangeTypeCmd.CobraCommand(fx.State()) fx.ExpectEnsureToken() - srv := &hcloud.Server{ID: 123, Name: "my-server", Datacenter: &hcloud.Datacenter{Location: &hcloud.Location{Name: "fsn1"}}} + srv := &hcloud.Server{ID: 123, Name: "my-server", Location: &hcloud.Location{Name: "fsn1"}} st := &hcloud.ServerType{ID: 456, Name: "cax21", Locations: []hcloud.ServerTypeLocation{{Location: &hcloud.Location{Name: "fsn1"}}}} fx.Client.ServerClient.EXPECT(). diff --git a/internal/cmd/server/create.go b/internal/cmd/server/create.go index 93e06e23..ab5da232 100644 --- a/internal/cmd/server/create.go +++ b/internal/cmd/server/create.go @@ -9,6 +9,7 @@ import ( "mime/multipart" "net/textproto" "os" + "strconv" "strings" "time" @@ -41,6 +42,10 @@ var CreateCmd = base.CreateCmd[*createResult]{ cmd := &cobra.Command{ Use: "create [options] --name --type --image ", Short: "Create a Server", + Long: `Create a Server. + +The --datacenter flag is deprecated. Use --location instead. +See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.`, } cmd.Flags().String("name", "", "Server name (required)") @@ -57,7 +62,7 @@ var CreateCmd = base.CreateCmd[*createResult]{ cmd.Flags().String("location", "", "Location (ID or name)") _ = cmd.RegisterFlagCompletionFunc("location", cmpl.SuggestCandidatesF(client.Location().Names)) - cmd.Flags().String("datacenter", "", "Datacenter (ID or name)") + cmd.Flags().String("datacenter", "", "Datacenter (ID or name) (deprecated)") _ = cmd.RegisterFlagCompletionFunc("datacenter", cmpl.SuggestCandidatesF(client.Datacenter().Names)) cmd.Flags().StringSlice("ssh-key", nil, "ID or name of SSH Key to inject (can be specified multiple times)") @@ -433,17 +438,40 @@ func createOptsFromFlags( } if datacenterIDOrName != "" { - var datacenter *hcloud.Datacenter - datacenter, _, err = s.Client().Datacenter().Get(s, datacenterIDOrName) - if err != nil { - return - } - if datacenter == nil { - err = fmt.Errorf("Datacenter not found: %s", datacenterIDOrName) - return + cmd.PrintErrln("Warning: The --datacenter flag is deprecated. Use --location instead.") + + // If parseable as ID -> GetByID and use Location Name + // Else -> Backwards-compatible split + var datacenterID int64 + datacenterID, err = strconv.ParseInt(datacenterIDOrName, 10, 64) + if err == nil { + // Input was a valid number/ID + var datacenter *hcloud.Datacenter + datacenter, _, err = s.Client().Datacenter().GetByID(s, datacenterID) + if err != nil { + return + } + if datacenter == nil { + err = fmt.Errorf("Datacenter not found: %s", datacenterIDOrName) + return + } + createOpts.Location = datacenter.Location + } else { + // Input was not a valid number/ID, probably DC name + + // Backward compatible datacenter argument. + // datacenter hel1-dc2 => location hel1 + parts := strings.Split(datacenterIDOrName, "-") + + if len(parts) != 2 { + err = fmt.Errorf("Datacenter name is not valid, expected format $LOCATION-$DATACENTER, but got: %s", datacenterIDOrName) + return + } + + createOpts.Location = &hcloud.Location{Name: parts[0]} } - createOpts.Datacenter = datacenter } + if locationIDOrName != "" { var location *hcloud.Location location, _, err = s.Client().Location().Get(s, locationIDOrName) diff --git a/internal/cmd/server/describe.go b/internal/cmd/server/describe.go index 01786634..cf308eaa 100644 --- a/internal/cmd/server/describe.go +++ b/internal/cmd/server/describe.go @@ -12,6 +12,7 @@ import ( "github.com/hetznercloud/cli/internal/cmd/datacenter" "github.com/hetznercloud/cli/internal/cmd/image" "github.com/hetznercloud/cli/internal/cmd/iso" + "github.com/hetznercloud/cli/internal/cmd/location" "github.com/hetznercloud/cli/internal/cmd/placementgroup" "github.com/hetznercloud/cli/internal/cmd/servertype" "github.com/hetznercloud/cli/internal/cmd/util" @@ -45,7 +46,7 @@ var DescribeCmd = base.DescribeCmd[*hcloud.Server]{ // As we already know the location the server is in, we can show the deprecation info // of that server type in that specific location. locationInfoIndex := slices.IndexFunc(server.ServerType.Locations, func(locInfo hcloud.ServerTypeLocation) bool { - return locInfo.Location.Name == server.Datacenter.Location.Name + return locInfo.Location.Name == server.Location.Name }) if locationInfoIndex >= 0 { if text := util.DescribeDeprecation(server.ServerType.Locations[locationInfoIndex]); text != "" { @@ -145,8 +146,14 @@ var DescribeCmd = base.DescribeCmd[*hcloud.Server]{ } fmt.Fprintln(out) - fmt.Fprintf(out, "Datacenter:\n") - fmt.Fprint(out, util.PrefixLines(datacenter.DescribeDatacenter(s.Client(), server.Datacenter, true), " ")) + fmt.Fprintf(out, "Location:\n") + fmt.Fprint(out, util.PrefixLines(location.DescribeLocation(server.Location), " ")) + + if server.Datacenter != nil { + fmt.Fprintln(out) + fmt.Fprintf(out, "Datacenter:\n") + fmt.Fprintf(out, "%s", util.PrefixLines(datacenter.DescribeDatacenter(s.Client(), server.Datacenter, true), " ")) + } fmt.Fprintln(out) if server.BackupWindow != "" { diff --git a/internal/cmd/server/describe_test.go b/internal/cmd/server/describe_test.go index ae4f4e6b..005ffa32 100644 --- a/internal/cmd/server/describe_test.go +++ b/internal/cmd/server/describe_test.go @@ -66,20 +66,15 @@ func TestDescribe(t *testing.T) { "key": "value", }, }, - Datacenter: &hcloud.Datacenter{ - ID: 4, - Name: "hel1-dc2", - Location: &hcloud.Location{ - ID: 3, - Name: "hel1", - Description: "Helsinki DC Park 1", - NetworkZone: "eu-central", - Country: "FI", - City: "Helsinki", - Latitude: 60.169855, - Longitude: 24.938379, - }, - Description: "Helsinki 1 virtual DC 2", + Location: &hcloud.Location{ + ID: 3, + Name: "hel1", + Description: "Helsinki DC Park 1", + NetworkZone: "eu-central", + Country: "FI", + City: "Helsinki", + Latitude: 60.169855, + Longitude: 24.938379, }, IncludedTraffic: 20 * util.Tebibyte, Protection: hcloud.ServerProtection{Delete: true, Rebuild: true}, @@ -149,20 +144,15 @@ Image: Labels: key: value -Datacenter: - ID: 4 - Name: hel1-dc2 - Description: Helsinki 1 virtual DC 2 - - Location: - ID: 3 - Name: hel1 - Description: Helsinki DC Park 1 - Network Zone: eu-central - Country: FI - City: Helsinki - Latitude: 60.169855 - Longitude: 24.938379 +Location: + ID: 3 + Name: hel1 + Description: Helsinki DC Park 1 + Network Zone: eu-central + Country: FI + City: Helsinki + Latitude: 60.169855 + Longitude: 24.938379 Backup Window: Backups disabled diff --git a/internal/cmd/server/list.go b/internal/cmd/server/list.go index 4f69adae..9e3d6c9f 100644 --- a/internal/cmd/server/list.go +++ b/internal/cmd/server/list.go @@ -7,7 +7,7 @@ import ( "strings" "time" - humanize "github.com/dustin/go-humanize" + "github.com/dustin/go-humanize" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -37,7 +37,7 @@ var serverStatusStrings = []string{ var ListCmd = &base.ListCmd[*hcloud.Server, schema.Server]{ ResourceNamePlural: "Servers", JSONKeyGetByName: "servers", - DefaultColumns: []string{"id", "name", "status", "ipv4", "ipv6", "private_net", "datacenter", "age"}, + DefaultColumns: []string{"id", "name", "status", "ipv4", "ipv6", "private_net", "location", "age"}, SortOption: config.OptionSortServer, AdditionalFlags: func(cmd *cobra.Command) { @@ -89,10 +89,14 @@ var ListCmd = &base.ListCmd[*hcloud.Server, schema.Server]{ return humanize.IBytes(server.OutgoingTraffic) }). AddFieldFn("datacenter", func(server *hcloud.Server) string { - return server.Datacenter.Name + if server.Datacenter != nil { + return server.Datacenter.Name + } + return "-" + }). AddFieldFn("location", func(server *hcloud.Server) string { - return server.Datacenter.Location.Name + return server.Location.Name }). AddFieldFn("labels", func(server *hcloud.Server) string { return util.LabelsToString(server.Labels) diff --git a/internal/cmd/server/list_test.go b/internal/cmd/server/list_test.go index 709d6034..b0625eb4 100644 --- a/internal/cmd/server/list_test.go +++ b/internal/cmd/server/list_test.go @@ -34,10 +34,10 @@ func TestList(t *testing.T) { ). Return([]*hcloud.Server{ { - ID: 123, - Name: "test", - Status: hcloud.ServerStatusRunning, - Datacenter: &hcloud.Datacenter{Name: "fsn1-dc14"}, + ID: 123, + Name: "test", + Status: hcloud.ServerStatusRunning, + Location: &hcloud.Location{Name: "fsn1"}, PublicNet: hcloud.ServerPublicNet{ IPv4: hcloud.ServerPublicNetIPv4{ IP: net.ParseIP("192.168.2.1"), @@ -49,8 +49,8 @@ func TestList(t *testing.T) { out, errOut, err := fx.Run(cmd, []string{"--status", "running"}) - expOut := `ID NAME STATUS IPV4 IPV6 PRIVATE NET DATACENTER AGE -123 test running 192.168.2.1 - - fsn1-dc14 20s + expOut := `ID NAME STATUS IPV4 IPV6 PRIVATE NET LOCATION AGE +123 test running 192.168.2.1 - - fsn1 20s ` require.NoError(t, err)