diff --git a/docs/reference/manual/hcloud_load-balancer_create.md b/docs/reference/manual/hcloud_load-balancer_create.md index 1f7a52378..25f437d69 100644 --- a/docs/reference/manual/hcloud_load-balancer_create.md +++ b/docs/reference/manual/hcloud_load-balancer_create.md @@ -18,6 +18,8 @@ hcloud load-balancer create [options] --name --type --network string Name or ID of the Network the Load Balancer should be attached to on creation --network-zone string Network Zone -o, --output stringArray output options: json|yaml + --primary-ipv4 string Primary IPv4 (ID or name) to assign to the Load Balancer + --primary-ipv6 string Primary IPv6 (ID or name) to assign to the Load Balancer --type string Load Balancer Type (ID or name) (required) ``` diff --git a/go.mod b/go.mod index 325383ce5..5b141793d 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/goccy/go-yaml v1.19.2 github.com/guptarohit/asciigraph v0.10.0 - github.com/hetznercloud/hcloud-go/v2 v2.50.0 + github.com/hetznercloud/hcloud-go/v2 v2.52.0 github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/spf13/cast v1.10.0 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index a7375376f..5a9694c7f 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/guptarohit/asciigraph v0.10.0 h1:LmbFXSHZOhaQxjJYexdRk7TzoC5sJ7vDTEjP1YUbKgY= github.com/guptarohit/asciigraph v0.10.0/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag= -github.com/hetznercloud/hcloud-go/v2 v2.50.0 h1:ECWYFFGTU0/ftWjTBDPDtqpqxEbCAJYG9KHWpHPUoDo= -github.com/hetznercloud/hcloud-go/v2 v2.50.0/go.mod h1:J9QH6j8pRH0K3+HlqgOlQ8abXagWTD/GpTkfra2et+g= +github.com/hetznercloud/hcloud-go/v2 v2.52.0 h1:BrQSFNuPhPZrWNdXKSEACX1hcS1TRzKSHMfFcXYRhDA= +github.com/hetznercloud/hcloud-go/v2 v2.52.0/go.mod h1:J9QH6j8pRH0K3+HlqgOlQ8abXagWTD/GpTkfra2et+g= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/internal/cmd/loadbalancer/create.go b/internal/cmd/loadbalancer/create.go index bbf35a1c9..dcf11999f 100644 --- a/internal/cmd/loadbalancer/create.go +++ b/internal/cmd/loadbalancer/create.go @@ -48,6 +48,12 @@ var CreateCmd = base.CreateCmd[*hcloud.LoadBalancer]{ cmd.Flags().String("network", "", "Name or ID of the Network the Load Balancer should be attached to on creation") _ = cmd.RegisterFlagCompletionFunc("network", cmpl.SuggestCandidatesF(client.Network().Names)) + cmd.Flags().String("primary-ipv4", "", "Primary IPv4 (ID or name) to assign to the Load Balancer") + _ = cmd.RegisterFlagCompletionFunc("primary-ipv4", cmpl.SuggestCandidatesF(client.PrimaryIP().Names(true, false, hcloud.Ptr(hcloud.PrimaryIPTypeIPv4)))) + + cmd.Flags().String("primary-ipv6", "", "Primary IPv6 (ID or name) to assign to the Load Balancer") + _ = cmd.RegisterFlagCompletionFunc("primary-ipv6", cmpl.SuggestCandidatesF(client.PrimaryIP().Names(true, false, hcloud.Ptr(hcloud.PrimaryIPTypeIPv6)))) + return cmd }, Run: func(s state.State, cmd *cobra.Command, _ []string) (*hcloud.LoadBalancer, any, error) { @@ -59,6 +65,8 @@ var CreateCmd = base.CreateCmd[*hcloud.LoadBalancer]{ labels, _ := cmd.Flags().GetStringToString("label") protection, _ := cmd.Flags().GetStringSlice("enable-protection") network, _ := cmd.Flags().GetString("network") + primaryIPv4IDOrName, _ := cmd.Flags().GetString("primary-ipv4") + primaryIPv6IDOrName, _ := cmd.Flags().GetString("primary-ipv6") protectionOpts, err := ChangeProtectionCmds.GetChangeProtectionOpts(true, protection) if err != nil { @@ -99,6 +107,30 @@ var CreateCmd = base.CreateCmd[*hcloud.LoadBalancer]{ } createOpts.Network = net } + publicNet := &hcloud.LoadBalancerCreateOptsPublicNet{} + if primaryIPv4IDOrName != "" { + primaryIPv4, _, err := s.Client().PrimaryIP().Get(s, primaryIPv4IDOrName) + if err != nil { + return nil, nil, err + } + if primaryIPv4 == nil { + return nil, nil, fmt.Errorf("Primary IPv4 not found: %s", primaryIPv4IDOrName) + } + publicNet.IPv4 = primaryIPv4 + } + if primaryIPv6IDOrName != "" { + primaryIPv6, _, err := s.Client().PrimaryIP().Get(s, primaryIPv6IDOrName) + if err != nil { + return nil, nil, err + } + if primaryIPv6 == nil { + return nil, nil, fmt.Errorf("Primary IPv6 not found: %s", primaryIPv6IDOrName) + } + publicNet.IPv6 = primaryIPv6 + } + if primaryIPv4IDOrName != "" || primaryIPv6IDOrName != "" { + createOpts.PublicNet = publicNet + } result, _, err := s.Client().LoadBalancer().Create(s, createOpts) if err != nil { return nil, nil, err diff --git a/internal/cmd/loadbalancer/create_test.go b/internal/cmd/loadbalancer/create_test.go index 4c37b6b09..87396d054 100644 --- a/internal/cmd/loadbalancer/create_test.go +++ b/internal/cmd/loadbalancer/create_test.go @@ -66,6 +66,83 @@ IPv6: :: assert.Equal(t, expOut, out) } +func TestCreatePrimaryIPs(t *testing.T) { + fx := testutil.NewFixture(t) + defer fx.Finish() + + cmd := loadbalancer.CreateCmd.CobraCommand(fx.State()) + fx.ExpectEnsureToken() + + primaryIPv4 := &hcloud.PrimaryIP{ + ID: 1, + Name: "myPrimaryIP", + Type: hcloud.PrimaryIPTypeIPv4, + IP: net.ParseIP("192.168.2.1"), + } + primaryIPv6 := &hcloud.PrimaryIP{ + ID: 2, + Name: "myPrimaryIPv6", + Type: hcloud.PrimaryIPTypeIPv6, + IP: net.IPv6zero, + } + + fx.Client.LoadBalancerTypeClient.EXPECT(). + Get(gomock.Any(), "lb11"). + Return(&hcloud.LoadBalancerType{Name: "lb11"}, nil, nil) + fx.Client.PrimaryIPClient.EXPECT(). + Get(gomock.Any(), "myPrimaryIP"). + Return(primaryIPv4, nil, nil) + fx.Client.PrimaryIPClient.EXPECT(). + Get(gomock.Any(), "myPrimaryIPv6"). + Return(primaryIPv6, nil, nil) + fx.Client.LoadBalancerClient.EXPECT(). + Create(gomock.Any(), hcloud.LoadBalancerCreateOpts{ + Name: "myLoadBalancer", + LoadBalancerType: &hcloud.LoadBalancerType{Name: "lb11"}, + Location: &hcloud.Location{Name: "fsn1"}, + Labels: make(map[string]string), + PublicNet: &hcloud.LoadBalancerCreateOptsPublicNet{ + IPv4: primaryIPv4, + IPv6: primaryIPv6, + }, + }). + Return(hcloud.LoadBalancerCreateResult{ + LoadBalancer: &hcloud.LoadBalancer{ID: 123}, + Action: &hcloud.Action{ID: 321}, + }, nil, nil) + fx.ActionWaiter.EXPECT().WaitForActions(gomock.Any(), gomock.Any(), &hcloud.Action{ID: 321}).Return(nil) + fx.Client.LoadBalancerClient.EXPECT(). + GetByID(gomock.Any(), int64(123)). + Return(&hcloud.LoadBalancer{ + ID: 123, + PublicNet: hcloud.LoadBalancerPublicNet{ + IPv4: hcloud.LoadBalancerPublicNetIPv4{ + IP: net.ParseIP("192.168.2.1"), + }, + IPv6: hcloud.LoadBalancerPublicNetIPv6{ + IP: net.IPv6zero, + }, + }, + }, nil, nil) + + out, errOut, err := fx.Run(cmd, []string{ + "--name", "myLoadBalancer", + "--type", "lb11", + "--location", "fsn1", + "--primary-ipv4", "myPrimaryIP", + "--primary-ipv6", "myPrimaryIPv6", + }) + + expOut := `Load Balancer 123 created +IPv4: 192.168.2.1 +IPv6: :: +` + + require.NoError(t, err) + assert.Empty(t, errOut) + assert.Equal(t, expOut, out) +} + func TestCreateJSON(t *testing.T) { fx := testutil.NewFixture(t) defer fx.Finish() diff --git a/internal/cmd/loadbalancer/delete.go b/internal/cmd/loadbalancer/delete.go index c6ce60708..d93ded567 100644 --- a/internal/cmd/loadbalancer/delete.go +++ b/internal/cmd/loadbalancer/delete.go @@ -18,7 +18,7 @@ var DeleteCmd = base.DeleteCmd[*hcloud.LoadBalancer]{ return s.Client().LoadBalancer().Get(s, idOrName) }, Delete: func(s state.State, _ *cobra.Command, loadBalancer *hcloud.LoadBalancer) ([]*hcloud.Action, error) { - _, err := s.Client().LoadBalancer().Delete(s, loadBalancer) - return nil, err + result, _, err := s.Client().LoadBalancer().DeleteWithResult(s, loadBalancer) + return []*hcloud.Action{result.Action}, err }, } diff --git a/internal/cmd/loadbalancer/delete_test.go b/internal/cmd/loadbalancer/delete_test.go index 50539b6c6..6207cafcd 100644 --- a/internal/cmd/loadbalancer/delete_test.go +++ b/internal/cmd/loadbalancer/delete_test.go @@ -28,8 +28,12 @@ func TestDelete(t *testing.T) { Get(gomock.Any(), "test"). Return(loadBalancer, nil, nil) fx.Client.LoadBalancerClient.EXPECT(). - Delete(gomock.Any(), loadBalancer). - Return(nil, nil) + DeleteWithResult(gomock.Any(), loadBalancer). + Return(hcloud.LoadBalancerDeleteResult{ + Action: &hcloud.Action{ID: 321}, + }, nil, nil) + fx.ActionWaiter.EXPECT(). + WaitForActions(gomock.Any(), gomock.Any(), &hcloud.Action{ID: 321}) out, errOut, err := fx.Run(cmd, []string{"test"}) @@ -69,10 +73,15 @@ func TestDeleteMultiple(t *testing.T) { Get(gomock.Any(), lb.Name). Return(lb, nil, nil) fx.Client.LoadBalancerClient.EXPECT(). - Delete(gomock.Any(), lb). - Return(nil, nil) + DeleteWithResult(gomock.Any(), lb). + Return(hcloud.LoadBalancerDeleteResult{ + Action: &hcloud.Action{ID: 321}, + }, nil, nil) } + fx.ActionWaiter.EXPECT(). + WaitForActions(gomock.Any(), gomock.Any(), &hcloud.Action{ID: 321}) + out, errOut, err := fx.Run(cmd, names) require.NoError(t, err) diff --git a/internal/cmd/loadbalancer/testdata/create_response.json b/internal/cmd/loadbalancer/testdata/create_response.json index 1f30b7130..a7755bbc5 100644 --- a/internal/cmd/loadbalancer/testdata/create_response.json +++ b/internal/cmd/loadbalancer/testdata/create_response.json @@ -39,11 +39,15 @@ "public_net": { "enabled": false, "ipv4": { + "blocked": false, "dns_ptr": "", + "id": 0, "ip": "192.168.2.1" }, "ipv6": { + "blocked": false, "dns_ptr": "", + "id": 0, "ip": "::" } }, diff --git a/internal/hcapi2/mock/zz_hcapi_mock.go b/internal/hcapi2/mock/zz_hcapi_mock.go index 5b30cf6a7..c36a29daf 100644 --- a/internal/hcapi2/mock/zz_hcapi_mock.go +++ b/internal/hcapi2/mock/zz_hcapi_mock.go @@ -2048,6 +2048,22 @@ func (mr *MockLoadBalancerClientMockRecorder) DeleteService(ctx, loadBalancer, l return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteService", reflect.TypeOf((*MockLoadBalancerClient)(nil).DeleteService), ctx, loadBalancer, listenPort) } +// DeleteWithResult mocks base method. +func (m *MockLoadBalancerClient) DeleteWithResult(ctx context.Context, loadBalancer *hcloud.LoadBalancer) (hcloud.LoadBalancerDeleteResult, *hcloud.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteWithResult", ctx, loadBalancer) + ret0, _ := ret[0].(hcloud.LoadBalancerDeleteResult) + ret1, _ := ret[1].(*hcloud.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// DeleteWithResult indicates an expected call of DeleteWithResult. +func (mr *MockLoadBalancerClientMockRecorder) DeleteWithResult(ctx, loadBalancer any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWithResult", reflect.TypeOf((*MockLoadBalancerClient)(nil).DeleteWithResult), ctx, loadBalancer) +} + // DetachFromNetwork mocks base method. func (m *MockLoadBalancerClient) DetachFromNetwork(ctx context.Context, loadBalancer *hcloud.LoadBalancer, opts hcloud.LoadBalancerDetachFromNetworkOpts) (*hcloud.Action, *hcloud.Response, error) { m.ctrl.T.Helper()