From 165ba5f1ddb3a55ec4481e2fc8749601fabc759f Mon Sep 17 00:00:00 2001 From: pauhull <22707808+pauhull@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:32:13 +0100 Subject: [PATCH] fix: use --poll-interval flag --- internal/cli/root.go | 9 ++ internal/hcapi2/action.go | 20 +++ internal/hcapi2/client.go | 146 +++++++++++------- internal/hcapi2/mock/client.go | 13 ++ internal/hcapi2/mock/mock_gen.go | 1 + internal/hcapi2/mock/zz_action_client_mock.go | 128 +++++++++++++++ internal/state/helpers.go | 4 +- internal/state/state.go | 15 +- 8 files changed, 265 insertions(+), 71 deletions(-) create mode 100644 internal/hcapi2/action.go create mode 100644 internal/hcapi2/mock/zz_action_client_mock.go diff --git a/internal/cli/root.go b/internal/cli/root.go index d0916bef..e467322b 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -27,6 +27,7 @@ import ( "github.com/hetznercloud/cli/internal/cmd/version" "github.com/hetznercloud/cli/internal/cmd/volume" "github.com/hetznercloud/cli/internal/state" + "github.com/hetznercloud/hcloud-go/v2/hcloud" ) func NewRootCommand(s state.State) *cobra.Command { @@ -63,5 +64,13 @@ func NewRootCommand(s state.State) *cobra.Command { ) cmd.PersistentFlags().Duration("poll-interval", 500*time.Millisecond, "Interval at which to poll information, for example action progress") cmd.SetOut(os.Stdout) + cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + pollInterval, err := cmd.Flags().GetDuration("poll-interval") + if err != nil { + return err + } + s.Client().WithOpts(hcloud.WithPollBackoffFunc(hcloud.ConstantBackoff(pollInterval))) + return nil + } return cmd } diff --git a/internal/hcapi2/action.go b/internal/hcapi2/action.go new file mode 100644 index 00000000..1e7561cc --- /dev/null +++ b/internal/hcapi2/action.go @@ -0,0 +1,20 @@ +package hcapi2 + +import ( + "github.com/hetznercloud/hcloud-go/v2/hcloud" +) + +// ActionClient embeds the Hetzner Cloud Action client +type ActionClient interface { + hcloud.IActionClient +} + +func NewActionClient(client hcloud.IActionClient) ActionClient { + return &actionClient{ + IActionClient: client, + } +} + +type actionClient struct { + hcloud.IActionClient +} diff --git a/internal/hcapi2/client.go b/internal/hcapi2/client.go index 14a1befa..c6f8d00e 100644 --- a/internal/hcapi2/client.go +++ b/internal/hcapi2/client.go @@ -8,6 +8,7 @@ import ( // Client makes all API clients accessible via a single interface. type Client interface { + Action() ActionClient Datacenter() DatacenterClient Firewall() FirewallClient FloatingIP() FloatingIPClient @@ -25,10 +26,11 @@ type Client interface { PlacementGroup() PlacementGroupClient RDNS() RDNSClient PrimaryIP() PrimaryIPClient + WithOpts(...hcloud.ClientOption) } -type client struct { - client *hcloud.Client +type clientCache struct { + actionClient ActionClient certificateClient CertificateClient datacenterClient DatacenterClient serverClient ServerClient @@ -46,162 +48,192 @@ type client struct { placementGroupClient PlacementGroupClient rdnsClient RDNSClient primaryIPClient PrimaryIPClient +} + +type client struct { + client *hcloud.Client + cache clientCache - mu sync.Mutex + mu sync.Mutex + opts []hcloud.ClientOption } // NewClient creates a new CLI API client extending hcloud.Client. -func NewClient(c *hcloud.Client) Client { - return &client{ - client: c, +func NewClient(opts ...hcloud.ClientOption) Client { + c := &client{ + opts: opts, + } + c.update() + return c +} + +func (c *client) WithOpts(opts ...hcloud.ClientOption) { + c.mu.Lock() + defer c.mu.Unlock() + c.opts = append(c.opts, opts...) + c.update() +} + +func (c *client) update() { + c.client = hcloud.NewClient(c.opts...) + c.cache = clientCache{} +} + +func (c *client) Action() ActionClient { + c.mu.Lock() + if c.cache.actionClient == nil { + c.cache.actionClient = NewActionClient(&c.client.Action) } + defer c.mu.Unlock() + return c.cache.actionClient } + func (c *client) Certificate() CertificateClient { c.mu.Lock() - if c.certificateClient == nil { - c.certificateClient = NewCertificateClient(&c.client.Certificate) + if c.cache.certificateClient == nil { + c.cache.certificateClient = NewCertificateClient(&c.client.Certificate) } defer c.mu.Unlock() - return c.certificateClient + return c.cache.certificateClient } func (c *client) Datacenter() DatacenterClient { c.mu.Lock() - if c.datacenterClient == nil { - c.datacenterClient = NewDatacenterClient(&c.client.Datacenter) + if c.cache.datacenterClient == nil { + c.cache.datacenterClient = NewDatacenterClient(&c.client.Datacenter) } defer c.mu.Unlock() - return c.datacenterClient + return c.cache.datacenterClient } func (c *client) Firewall() FirewallClient { c.mu.Lock() - if c.firewallClient == nil { - c.firewallClient = NewFirewallClient(&c.client.Firewall) + if c.cache.firewallClient == nil { + c.cache.firewallClient = NewFirewallClient(&c.client.Firewall) } defer c.mu.Unlock() - return c.firewallClient + return c.cache.firewallClient } func (c *client) FloatingIP() FloatingIPClient { c.mu.Lock() - if c.floatingIPClient == nil { - c.floatingIPClient = NewFloatingIPClient(&c.client.FloatingIP) + if c.cache.floatingIPClient == nil { + c.cache.floatingIPClient = NewFloatingIPClient(&c.client.FloatingIP) } defer c.mu.Unlock() - return c.floatingIPClient + return c.cache.floatingIPClient } func (c *client) PrimaryIP() PrimaryIPClient { c.mu.Lock() - if c.primaryIPClient == nil { - c.primaryIPClient = NewPrimaryIPClient(&c.client.PrimaryIP) + if c.cache.primaryIPClient == nil { + c.cache.primaryIPClient = NewPrimaryIPClient(&c.client.PrimaryIP) } defer c.mu.Unlock() - return c.primaryIPClient + return c.cache.primaryIPClient } func (c *client) Image() ImageClient { c.mu.Lock() - if c.imageClient == nil { - c.imageClient = NewImageClient(&c.client.Image) + if c.cache.imageClient == nil { + c.cache.imageClient = NewImageClient(&c.client.Image) } defer c.mu.Unlock() - return c.imageClient + return c.cache.imageClient } func (c *client) ISO() ISOClient { c.mu.Lock() - if c.isoClient == nil { - c.isoClient = NewISOClient(&c.client.ISO) + if c.cache.isoClient == nil { + c.cache.isoClient = NewISOClient(&c.client.ISO) } defer c.mu.Unlock() - return c.isoClient + return c.cache.isoClient } func (c *client) Location() LocationClient { c.mu.Lock() - if c.locationClient == nil { - c.locationClient = NewLocationClient(&c.client.Location) + if c.cache.locationClient == nil { + c.cache.locationClient = NewLocationClient(&c.client.Location) } defer c.mu.Unlock() - return c.locationClient + return c.cache.locationClient } func (c *client) LoadBalancer() LoadBalancerClient { c.mu.Lock() - if c.loadBalancerClient == nil { - c.loadBalancerClient = NewLoadBalancerClient(&c.client.LoadBalancer) + if c.cache.loadBalancerClient == nil { + c.cache.loadBalancerClient = NewLoadBalancerClient(&c.client.LoadBalancer) } defer c.mu.Unlock() - return c.loadBalancerClient + return c.cache.loadBalancerClient } func (c *client) LoadBalancerType() LoadBalancerTypeClient { c.mu.Lock() - if c.loadBalancerTypeClient == nil { - c.loadBalancerTypeClient = NewLoadBalancerTypeClient(&c.client.LoadBalancerType) + if c.cache.loadBalancerTypeClient == nil { + c.cache.loadBalancerTypeClient = NewLoadBalancerTypeClient(&c.client.LoadBalancerType) } defer c.mu.Unlock() - return c.loadBalancerTypeClient + return c.cache.loadBalancerTypeClient } func (c *client) Network() NetworkClient { c.mu.Lock() - if c.networkClient == nil { - c.networkClient = NewNetworkClient(&c.client.Network) + if c.cache.networkClient == nil { + c.cache.networkClient = NewNetworkClient(&c.client.Network) } defer c.mu.Unlock() - return c.networkClient + return c.cache.networkClient } func (c *client) Server() ServerClient { c.mu.Lock() - if c.serverClient == nil { - c.serverClient = NewServerClient(&c.client.Server) + if c.cache.serverClient == nil { + c.cache.serverClient = NewServerClient(&c.client.Server) } defer c.mu.Unlock() - return c.serverClient + return c.cache.serverClient } func (c *client) ServerType() ServerTypeClient { c.mu.Lock() - if c.serverTypeClient == nil { - c.serverTypeClient = NewServerTypeClient(&c.client.ServerType) + if c.cache.serverTypeClient == nil { + c.cache.serverTypeClient = NewServerTypeClient(&c.client.ServerType) } defer c.mu.Unlock() - return c.serverTypeClient + return c.cache.serverTypeClient } func (c *client) SSHKey() SSHKeyClient { c.mu.Lock() - if c.sshKeyClient == nil { - c.sshKeyClient = NewSSHKeyClient(&c.client.SSHKey) + if c.cache.sshKeyClient == nil { + c.cache.sshKeyClient = NewSSHKeyClient(&c.client.SSHKey) } defer c.mu.Unlock() - return c.sshKeyClient + return c.cache.sshKeyClient } func (c *client) RDNS() RDNSClient { c.mu.Lock() - if c.rdnsClient == nil { - c.rdnsClient = NewRDNSClient(&c.client.RDNS) + if c.cache.rdnsClient == nil { + c.cache.rdnsClient = NewRDNSClient(&c.client.RDNS) } defer c.mu.Unlock() - return c.rdnsClient + return c.cache.rdnsClient } func (c *client) Volume() VolumeClient { c.mu.Lock() - if c.volumeClient == nil { - c.volumeClient = NewVolumeClient(&c.client.Volume) + if c.cache.volumeClient == nil { + c.cache.volumeClient = NewVolumeClient(&c.client.Volume) } defer c.mu.Unlock() - return c.volumeClient + return c.cache.volumeClient } func (c *client) PlacementGroup() PlacementGroupClient { c.mu.Lock() - if c.placementGroupClient == nil { - c.placementGroupClient = NewPlacementGroupClient(&c.client.PlacementGroup) + if c.cache.placementGroupClient == nil { + c.cache.placementGroupClient = NewPlacementGroupClient(&c.client.PlacementGroup) } defer c.mu.Unlock() - return c.placementGroupClient + return c.cache.placementGroupClient } diff --git a/internal/hcapi2/mock/client.go b/internal/hcapi2/mock/client.go index 7cf7886b..25053cff 100644 --- a/internal/hcapi2/mock/client.go +++ b/internal/hcapi2/mock/client.go @@ -4,9 +4,11 @@ import ( "github.com/golang/mock/gomock" "github.com/hetznercloud/cli/internal/hcapi2" + "github.com/hetznercloud/hcloud-go/v2/hcloud" ) type MockClient struct { + ActionClient *MockActionClient CertificateClient *MockCertificateClient DatacenterClient *MockDatacenterClient FirewallClient *MockFirewallClient @@ -28,6 +30,7 @@ type MockClient struct { func NewMockClient(ctrl *gomock.Controller) *MockClient { return &MockClient{ + ActionClient: NewMockActionClient(ctrl), CertificateClient: NewMockCertificateClient(ctrl), DatacenterClient: NewMockDatacenterClient(ctrl), FirewallClient: NewMockFirewallClient(ctrl), @@ -47,9 +50,15 @@ func NewMockClient(ctrl *gomock.Controller) *MockClient { RDNSClient: NewMockRDNSClient(ctrl), } } + +func (c *MockClient) Action() hcapi2.ActionClient { + return c.ActionClient +} + func (c *MockClient) Certificate() hcapi2.CertificateClient { return c.CertificateClient } + func (c *MockClient) Datacenter() hcapi2.DatacenterClient { return c.DatacenterClient } @@ -112,3 +121,7 @@ func (c *MockClient) RDNS() hcapi2.RDNSClient { func (c *MockClient) PlacementGroup() hcapi2.PlacementGroupClient { return c.PlacementGroupClient } + +func (*MockClient) WithOpts(...hcloud.ClientOption) { + // no-op +} diff --git a/internal/hcapi2/mock/mock_gen.go b/internal/hcapi2/mock/mock_gen.go index a38c491b..cbf54914 100644 --- a/internal/hcapi2/mock/mock_gen.go +++ b/internal/hcapi2/mock/mock_gen.go @@ -1,5 +1,6 @@ package hcapi2_mock +//go:generate mockgen -package hcapi2_mock -destination zz_action_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 ActionClient //go:generate mockgen -package hcapi2_mock -destination zz_certificate_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 CertificateClient //go:generate mockgen -package hcapi2_mock -destination zz_datacenter_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 DatacenterClient //go:generate mockgen -package hcapi2_mock -destination zz_image_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 ImageClient diff --git a/internal/hcapi2/mock/zz_action_client_mock.go b/internal/hcapi2/mock/zz_action_client_mock.go new file mode 100644 index 00000000..a3696011 --- /dev/null +++ b/internal/hcapi2/mock/zz_action_client_mock.go @@ -0,0 +1,128 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/hetznercloud/cli/internal/hcapi2 (interfaces: ActionClient) + +// Package hcapi2_mock is a generated GoMock package. +package hcapi2_mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + hcloud "github.com/hetznercloud/hcloud-go/v2/hcloud" +) + +// MockActionClient is a mock of ActionClient interface. +type MockActionClient struct { + ctrl *gomock.Controller + recorder *MockActionClientMockRecorder +} + +// MockActionClientMockRecorder is the mock recorder for MockActionClient. +type MockActionClientMockRecorder struct { + mock *MockActionClient +} + +// NewMockActionClient creates a new mock instance. +func NewMockActionClient(ctrl *gomock.Controller) *MockActionClient { + mock := &MockActionClient{ctrl: ctrl} + mock.recorder = &MockActionClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockActionClient) EXPECT() *MockActionClientMockRecorder { + return m.recorder +} + +// All mocks base method. +func (m *MockActionClient) All(arg0 context.Context) ([]*hcloud.Action, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "All", arg0) + ret0, _ := ret[0].([]*hcloud.Action) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// All indicates an expected call of All. +func (mr *MockActionClientMockRecorder) All(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockActionClient)(nil).All), arg0) +} + +// AllWithOpts mocks base method. +func (m *MockActionClient) AllWithOpts(arg0 context.Context, arg1 hcloud.ActionListOpts) ([]*hcloud.Action, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AllWithOpts", arg0, arg1) + ret0, _ := ret[0].([]*hcloud.Action) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AllWithOpts indicates an expected call of AllWithOpts. +func (mr *MockActionClientMockRecorder) AllWithOpts(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllWithOpts", reflect.TypeOf((*MockActionClient)(nil).AllWithOpts), arg0, arg1) +} + +// GetByID mocks base method. +func (m *MockActionClient) GetByID(arg0 context.Context, arg1 int64) (*hcloud.Action, *hcloud.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByID", arg0, arg1) + ret0, _ := ret[0].(*hcloud.Action) + ret1, _ := ret[1].(*hcloud.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetByID indicates an expected call of GetByID. +func (mr *MockActionClientMockRecorder) GetByID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockActionClient)(nil).GetByID), arg0, arg1) +} + +// List mocks base method. +func (m *MockActionClient) List(arg0 context.Context, arg1 hcloud.ActionListOpts) ([]*hcloud.Action, *hcloud.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1) + ret0, _ := ret[0].([]*hcloud.Action) + ret1, _ := ret[1].(*hcloud.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// List indicates an expected call of List. +func (mr *MockActionClientMockRecorder) List(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockActionClient)(nil).List), arg0, arg1) +} + +// WatchOverallProgress mocks base method. +func (m *MockActionClient) WatchOverallProgress(arg0 context.Context, arg1 []*hcloud.Action) (<-chan int, <-chan error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WatchOverallProgress", arg0, arg1) + ret0, _ := ret[0].(<-chan int) + ret1, _ := ret[1].(<-chan error) + return ret0, ret1 +} + +// WatchOverallProgress indicates an expected call of WatchOverallProgress. +func (mr *MockActionClientMockRecorder) WatchOverallProgress(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchOverallProgress", reflect.TypeOf((*MockActionClient)(nil).WatchOverallProgress), arg0, arg1) +} + +// WatchProgress mocks base method. +func (m *MockActionClient) WatchProgress(arg0 context.Context, arg1 *hcloud.Action) (<-chan int, <-chan error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WatchProgress", arg0, arg1) + ret0, _ := ret[0].(<-chan int) + ret1, _ := ret[1].(<-chan error) + return ret0, ret1 +} + +// WatchProgress indicates an expected call of WatchProgress. +func (mr *MockActionClientMockRecorder) WatchProgress(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchProgress", reflect.TypeOf((*MockActionClient)(nil).WatchProgress), arg0, arg1) +} diff --git a/internal/state/helpers.go b/internal/state/helpers.go index 4dcde3d5..25a00c19 100644 --- a/internal/state/helpers.go +++ b/internal/state/helpers.go @@ -34,7 +34,7 @@ func (c *state) ActionProgress(cmd *cobra.Command, ctx context.Context, action * } func (c *state) ActionsProgresses(cmd *cobra.Command, ctx context.Context, actions []*hcloud.Action) error { - progressCh, errCh := c.hcloudClient.Action.WatchOverallProgress(ctx, actions) + progressCh, errCh := c.Client().Action().WatchOverallProgress(ctx, actions) if StdoutIsTerminal() { progress := pb.New(100) @@ -83,7 +83,7 @@ func (c *state) WaitForActions(cmd *cobra.Command, ctx context.Context, actions waitingFor = fmt.Sprintf("Waiting for volume %d to have been attached to server %d", resources["volume"], resources["server"]) } - _, errCh := c.hcloudClient.Action.WatchProgress(ctx, action) + _, errCh := c.Client().Action().WatchProgress(ctx, action) err := DisplayProgressCircle(cmd, errCh, waitingFor) if err != nil { diff --git a/internal/state/state.go b/internal/state/state.go index 06619360..b7fd86df 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -4,7 +4,6 @@ import ( "context" "log" "os" - "time" "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/cli/internal/version" @@ -29,7 +28,6 @@ type state struct { debug bool debugFilePath string client hcapi2.Client - hcloudClient *hcloud.Client config *Config } @@ -53,8 +51,7 @@ func New(cfg *Config) (State, error) { } s.readEnv() - s.hcloudClient = s.newClient() - s.client = hcapi2.NewClient(s.hcloudClient) + s.client = s.newClient() return s, nil } @@ -112,7 +109,7 @@ func (c *state) readEnv() { } } -func (c *state) newClient() *hcloud.Client { +func (c *state) newClient() hcapi2.Client { opts := []hcloud.ClientOption{ hcloud.WithToken(c.token), hcloud.WithApplication("hcloud-cli", version.Version), @@ -128,11 +125,5 @@ func (c *state) newClient() *hcloud.Client { opts = append(opts, hcloud.WithDebugWriter(writer)) } } - // TODO Somehow pass here - // pollInterval, _ := c.RootCommand.PersistentFlags().GetDuration("poll-interval") - pollInterval := 500 * time.Millisecond - if pollInterval > 0 { - opts = append(opts, hcloud.WithPollInterval(pollInterval)) - } - return hcloud.NewClient(opts...) + return hcapi2.NewClient(opts...) }