diff --git a/go.mod b/go.mod index 2e8f8f70..2aed46e0 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/jmattheis/goverter v1.9.3 github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 - github.com/vburenin/ifacemaker v1.3.0 + github.com/vburenin/ifacemaker v1.3.1-0.20251209121141-a6d9756091ba golang.org/x/crypto v0.46.0 golang.org/x/net v0.48.0 ) @@ -30,11 +30,11 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index afe47a42..6a42ccf6 100644 --- a/go.sum +++ b/go.sum @@ -37,16 +37,16 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/vburenin/ifacemaker v1.3.0 h1:X5//v/1tyORf5157wLATgP1wgquW3FUW91/OGHLRqGo= -github.com/vburenin/ifacemaker v1.3.0/go.mod h1:SxTD9m+6uBQyhd0aohV7R4iirO+l9mEoTn4nSe67vMs= +github.com/vburenin/ifacemaker v1.3.1-0.20251209121141-a6d9756091ba h1:rc8YUE3u6tgdBqjKvZCrCcqPxczqoFnu5oWofin92Yw= +github.com/vburenin/ifacemaker v1.3.1-0.20251209121141-a6d9756091ba/go.mod h1:Car3Ss6dMkORtaciIcBu5DlzKQaawLS5f4nogJjRHr0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= @@ -57,8 +57,8 @@ golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hcloud/action.go b/hcloud/action.go index 59388ae4..e6b34802 100644 --- a/hcloud/action.go +++ b/hcloud/action.go @@ -86,7 +86,7 @@ func (a *Action) Error() error { // ActionClient is a client for the actions API. type ActionClient struct { - action *ResourceActionClient + action *ResourceActionClient[noopResource] } // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. @@ -116,7 +116,7 @@ func (l ActionListOpts) values() url.Values { return vals } -// List returns a list of actions for a specific page. +// List returns a paginated list of actions. // // Please note that filters specified in opts are not taken into account // when their value corresponds to their zero value or when they are empty. @@ -143,12 +143,12 @@ func (c *ActionClient) AllWithOpts(ctx context.Context, opts ActionListOpts) ([] } // ResourceActionClient is a client for the actions API exposed by the resource. -type ResourceActionClient struct { +type ResourceActionClient[R actionSupporter] struct { resource string client *Client } -func (c *ResourceActionClient) getBaseURL() string { +func (c *ResourceActionClient[R]) getBaseURL() string { if c.resource == "" { return "" } @@ -157,7 +157,7 @@ func (c *ResourceActionClient) getBaseURL() string { } // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. -func (c *ResourceActionClient) GetByID(ctx context.Context, id int64) (*Action, *Response, error) { +func (c *ResourceActionClient[R]) GetByID(ctx context.Context, id int64) (*Action, *Response, error) { opPath := c.getBaseURL() + "/actions/%d" ctx = ctxutil.SetOpPath(ctx, opPath) @@ -173,11 +173,11 @@ func (c *ResourceActionClient) GetByID(ctx context.Context, id int64) (*Action, return ActionFromSchema(respBody.Action), resp, nil } -// List returns a list of actions for a specific page. +// List returns a paginated list of actions. // // Please note that filters specified in opts are not taken into account // when their value corresponds to their zero value or when they are empty. -func (c *ResourceActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) { +func (c *ResourceActionClient[R]) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) { opPath := c.getBaseURL() + "/actions?%s" ctx = ctxutil.SetOpPath(ctx, opPath) @@ -192,7 +192,7 @@ func (c *ResourceActionClient) List(ctx context.Context, opts ActionListOpts) ([ } // All returns all actions for the given options. -func (c *ResourceActionClient) All(ctx context.Context, opts ActionListOpts) ([]*Action, error) { +func (c *ResourceActionClient[R]) All(ctx context.Context, opts ActionListOpts) ([]*Action, error) { if opts.ListOpts.PerPage == 0 { opts.ListOpts.PerPage = 50 } @@ -201,3 +201,44 @@ func (c *ResourceActionClient) All(ctx context.Context, opts ActionListOpts) ([] return c.List(ctx, opts) }) } + +type actionSupporter interface { + pathID() (string, error) +} + +// noopResource is used by the [ActionClient] to satisfy its underlying +// [ResourceActionClient] generic type. +type noopResource struct{} + +func (noopResource) pathID() (string, error) { return "", nil } + +// ListFor returns a paginated list of actions for the given Resource. +// +// Please note that filters specified in opts are not taken into account +// when their value corresponds to their zero value or when they are empty. +func (c *ResourceActionClient[R]) ListFor(ctx context.Context, resource R, opts ActionListOpts) ([]*Action, *Response, error) { + opPath := c.getBaseURL() + "/%s/actions?%s" + ctx = ctxutil.SetOpPath(ctx, opPath) + + id, err := resource.pathID() + if err != nil { + return nil, nil, invalidArgument("resource", resource, err) + } + + reqPath := fmt.Sprintf(opPath, id, opts.values().Encode()) + + respBody, resp, err := getRequest[schema.ActionListResponse](ctx, c.client, reqPath) + if err != nil { + return nil, resp, err + } + + return allFromSchemaFunc(respBody.Actions, ActionFromSchema), resp, nil +} + +// AllFor returns all actions for the given Resource. +func (c *ResourceActionClient[R]) AllFor(ctx context.Context, resource R, opts ActionListOpts) ([]*Action, error) { + return iterPages(func(page int) ([]*Action, *Response, error) { + opts.Page = page + return c.ListFor(ctx, resource, opts) + }) +} diff --git a/hcloud/action_test.go b/hcloud/action_test.go index 111e9cdb..15954eee 100644 --- a/hcloud/action_test.go +++ b/hcloud/action_test.go @@ -3,12 +3,15 @@ package hcloud import ( "context" "encoding/json" + "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/mockutil" "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" ) @@ -330,3 +333,189 @@ func TestResourceActionClientAll(t *testing.T) { t.Errorf("unexpected actions") } } + +func TestResourceActionClientListFor(t *testing.T) { + t.Run("minimal", func(t *testing.T) { + ctx, server, client := makeTestUtils(t) + + server.Expect([]mockutil.Request{ + { + Method: "GET", Path: "/primary_ips/13/actions?", + Status: 200, + JSONRaw: `{ + "actions": [ + { "id": 1509772237 } + ] + }`, + }, + }) + + result, resp, err := client.PrimaryIP.Action.ListFor(ctx, &PrimaryIP{ID: 13}, ActionListOpts{}) + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, result, 1) + require.Equal(t, int64(1509772237), result[0].ID) + }) + + t.Run("full", func(t *testing.T) { + ctx, server, client := makeTestUtils(t) + + server.Expect([]mockutil.Request{ + { + Method: "GET", Path: "/primary_ips/13/actions?page=2&per_page=50&sort=asc%3Aid&status=running", + Status: 200, + JSONRaw: `{ + "actions": [ + { "id": 1509772237 } + ] + }`, + }, + }) + + result, resp, err := client.PrimaryIP.Action.ListFor(ctx, &PrimaryIP{ID: 13}, ActionListOpts{ + Status: []ActionStatus{ActionStatusRunning}, + Sort: []string{"asc:id"}, + ListOpts: ListOpts{ + Page: 2, + PerPage: 50, + }, + }) + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, result, 1) + require.Equal(t, int64(1509772237), result[0].ID) + }) + + t.Run("string resource id", func(t *testing.T) { + ctx, server, client := makeTestUtils(t) + + server.Expect([]mockutil.Request{ + { + Method: "GET", Path: "/zones/example.org/actions?", + Status: 200, + JSONRaw: `{ + "actions": [ + { "id": 1509772237 } + ] + }`, + }, + }) + + result, resp, err := client.Zone.Action.ListFor(ctx, &Zone{Name: "example.org"}, ActionListOpts{}) + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, result, 1) + require.Equal(t, int64(1509772237), result[0].ID) + }) + + t.Run("invalid argument", func(t *testing.T) { + ctx, _, client := makeTestUtils(t) + + result, resp, err := client.PrimaryIP.Action.ListFor(ctx, &PrimaryIP{ID: 0}, ActionListOpts{}) + require.EqualError(t, err, "invalid argument 'resource' [*hcloud.PrimaryIP]: missing field [ID] in [*hcloud.PrimaryIP]") + require.Nil(t, result) + require.Nil(t, resp) + }) +} + +func TestResourceActionClientAllFor(t *testing.T) { + t.Run("minimal", func(t *testing.T) { + ctx, server, client := makeTestUtils(t) + + server.Expect([]mockutil.Request{ + { + Method: "GET", Path: "/primary_ips/13/actions?page=1", + Status: 200, + JSONRaw: `{ + "actions": [ + { "id": 1509772237 } + ], + "meta": { "pagination": { "page": 1, "next_page": 2 }} + }`, + }, + { + Method: "GET", Path: "/primary_ips/13/actions?page=2", + Status: 200, + JSONRaw: `{ + "actions": [ + { "id": 1509772238 } + ], + "meta": { "pagination": { "page": 2 }} + }`, + }, + }) + + result, err := client.PrimaryIP.Action.AllFor(ctx, &PrimaryIP{ID: 13}, ActionListOpts{}) + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, int64(1509772237), result[0].ID) + require.Equal(t, int64(1509772238), result[1].ID) + }) + + t.Run("full", func(t *testing.T) { + ctx, server, client := makeTestUtils(t) + + server.Expect([]mockutil.Request{ + { + Method: "GET", Path: "/primary_ips/13/actions?page=1&per_page=50&sort=asc%3Aid&status=running", + Status: 200, + JSONRaw: `{ + "actions": [ + { "id": 1509772237 } + ], + "meta": { "pagination": { "page": 1, "next_page": 2 }} + }`, + }, + { + Method: "GET", Path: "/primary_ips/13/actions?page=2&per_page=50&sort=asc%3Aid&status=running", + Status: 200, + JSONRaw: `{ + "actions": [ + { "id": 1509772238 } + ], + "meta": { "pagination": { "page": 2 }} + }`, + }, + }) + + result, err := client.PrimaryIP.Action.AllFor(ctx, &PrimaryIP{ID: 13}, ActionListOpts{ + Status: []ActionStatus{ActionStatusRunning}, + Sort: []string{"asc:id"}, + ListOpts: ListOpts{ + PerPage: 50, + }, + }) + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, int64(1509772237), result[0].ID) + require.Equal(t, int64(1509772238), result[1].ID) + }) +} + +func ExampleResourceActionClient_ListFor() { + ctx := context.Background() + client := NewClient() + + { + server := &Server{ID: 5425271} + + // List actions for the server 5425271. + result, _, _ := client.Server.Action.ListFor(ctx, server, ActionListOpts{}) + // Error handling skipped + + for _, action := range result { + fmt.Println(action.ID, action.Command, action.Status) + } + } + { + zone := &Zone{Name: "example.org"} + + // List actions for the zone "example.org". + result, _, _ := client.Zone.Action.ListFor(ctx, zone, ActionListOpts{}) + // Error handling skipped + + for _, action := range result { + fmt.Println(action.ID, action.Command, action.Status) + } + } +} diff --git a/hcloud/certificate.go b/hcloud/certificate.go index e4d821be..c115285d 100644 --- a/hcloud/certificate.go +++ b/hcloud/certificate.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/url" + "strconv" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" @@ -81,6 +82,13 @@ type Certificate struct { UsedBy []CertificateUsedByRef } +func (o *Certificate) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // CertificateCreateResult is the result of creating a certificate. type CertificateCreateResult struct { Certificate *Certificate @@ -90,7 +98,7 @@ type CertificateCreateResult struct { // CertificateClient is a client for the Certificates API. type CertificateClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*Certificate] } // GetByID retrieves a Certificate by its ID. If the Certificate does not exist, nil is returned. diff --git a/hcloud/client.go b/hcloud/client.go index 542598e3..46c2c983 100644 --- a/hcloud/client.go +++ b/hcloud/client.go @@ -291,26 +291,26 @@ func NewClient(options ...ClientOption) *Client { client.handler = assembleHandlerChain(client) // Cloud API - client.Action = ActionClient{action: &ResourceActionClient{client: client}} + client.Action = ActionClient{action: &ResourceActionClient[noopResource]{client: client}} client.Datacenter = DatacenterClient{client: client} - client.FloatingIP = FloatingIPClient{client: client, Action: &ResourceActionClient{client: client, resource: "floating_ips"}} - client.Image = ImageClient{client: client, Action: &ResourceActionClient{client: client, resource: "images"}} + client.FloatingIP = FloatingIPClient{client: client, Action: &ResourceActionClient[*FloatingIP]{client: client, resource: "floating_ips"}} + client.Image = ImageClient{client: client, Action: &ResourceActionClient[*Image]{client: client, resource: "images"}} client.ISO = ISOClient{client: client} client.Location = LocationClient{client: client} - client.Network = NetworkClient{client: client, Action: &ResourceActionClient{client: client, resource: "networks"}} + client.Network = NetworkClient{client: client, Action: &ResourceActionClient[*Network]{client: client, resource: "networks"}} client.Pricing = PricingClient{client: client} - client.Server = ServerClient{client: client, Action: &ResourceActionClient{client: client, resource: "servers"}} + client.Server = ServerClient{client: client, Action: &ResourceActionClient[*Server]{client: client, resource: "servers"}} client.ServerType = ServerTypeClient{client: client} client.SSHKey = SSHKeyClient{client: client} - client.Volume = VolumeClient{client: client, Action: &ResourceActionClient{client: client, resource: "volumes"}} - client.LoadBalancer = LoadBalancerClient{client: client, Action: &ResourceActionClient{client: client, resource: "load_balancers"}} + client.Volume = VolumeClient{client: client, Action: &ResourceActionClient[*Volume]{client: client, resource: "volumes"}} + client.LoadBalancer = LoadBalancerClient{client: client, Action: &ResourceActionClient[*LoadBalancer]{client: client, resource: "load_balancers"}} client.LoadBalancerType = LoadBalancerTypeClient{client: client} - client.Certificate = CertificateClient{client: client, Action: &ResourceActionClient{client: client, resource: "certificates"}} - client.Firewall = FirewallClient{client: client, Action: &ResourceActionClient{client: client, resource: "firewalls"}} + client.Certificate = CertificateClient{client: client, Action: &ResourceActionClient[*Certificate]{client: client, resource: "certificates"}} + client.Firewall = FirewallClient{client: client, Action: &ResourceActionClient[*Firewall]{client: client, resource: "firewalls"}} client.PlacementGroup = PlacementGroupClient{client: client} client.RDNS = RDNSClient{client: client} - client.PrimaryIP = PrimaryIPClient{client: client, Action: &ResourceActionClient{client: client, resource: "primary_ips"}} - client.Zone = ZoneClient{client: client, Action: &ResourceActionClient{client: client, resource: "zones"}} + client.PrimaryIP = PrimaryIPClient{client: client, Action: &ResourceActionClient[*PrimaryIP]{client: client, resource: "primary_ips"}} + client.Zone = ZoneClient{client: client, Action: &ResourceActionClient[*Zone]{client: client, resource: "zones"}} // Hetzner API @@ -321,7 +321,7 @@ func NewClient(options ...ClientOption) *Client { *hetznerClient = *client hetznerClient.endpoint = hetznerClient.hetznerEndpoint - client.StorageBox = StorageBoxClient{client: hetznerClient, Action: &ResourceActionClient{client: hetznerClient, resource: "storage_boxes"}} + client.StorageBox = StorageBoxClient{client: hetznerClient, Action: &ResourceActionClient[*StorageBox]{client: hetznerClient, resource: "storage_boxes"}} client.StorageBoxType = StorageBoxTypeClient{client: hetznerClient} return client diff --git a/hcloud/firewall.go b/hcloud/firewall.go index 261d9537..cd936e56 100644 --- a/hcloud/firewall.go +++ b/hcloud/firewall.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/url" + "strconv" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" @@ -21,6 +22,13 @@ type Firewall struct { AppliedTo []FirewallResource } +func (o *Firewall) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // FirewallRule represents a Firewall's rules. type FirewallRule struct { Direction FirewallRuleDirection @@ -91,7 +99,7 @@ type FirewallResourceLabelSelector struct { // FirewallClient is a client for the Firewalls API. type FirewallClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*Firewall] } // GetByID retrieves a Firewall by its ID. If the Firewall does not exist, nil is returned. diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go index 12a898cb..de7b7938 100644 --- a/hcloud/floating_ip.go +++ b/hcloud/floating_ip.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/url" + "strconv" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" @@ -28,6 +29,13 @@ type FloatingIP struct { Name string } +func (o *FloatingIP) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // DNSPtrForIP returns the reverse DNS pointer of the IP address. // // Deprecated: Use GetDNSPtrForIP instead. @@ -84,7 +92,7 @@ func (o *FloatingIP) GetDNSPtrForIP(ip net.IP) (string, error) { // FloatingIPClient is a client for the Floating IP API. type FloatingIPClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*FloatingIP] } // GetByID retrieves a Floating IP by its ID. If the Floating IP does not exist, diff --git a/hcloud/image.go b/hcloud/image.go index f080eccb..8ba97004 100644 --- a/hcloud/image.go +++ b/hcloud/image.go @@ -35,6 +35,13 @@ type Image struct { Deleted time.Time } +func (o *Image) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // IsDeprecated returns whether the image is deprecated. func (o *Image) IsDeprecated() bool { return !o.Deprecated.IsZero() @@ -77,7 +84,7 @@ const ( // ImageClient is a client for the image API. type ImageClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*Image] } // GetByID retrieves an image by its ID. If the image does not exist, nil is returned. diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go index a377f549..473e07b5 100644 --- a/hcloud/load_balancer.go +++ b/hcloud/load_balancer.go @@ -32,6 +32,13 @@ type LoadBalancer struct { IngoingTraffic uint64 } +func (o *LoadBalancer) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // LoadBalancerPublicNet represents a Load Balancer's public network. type LoadBalancerPublicNet struct { Enabled bool @@ -244,7 +251,7 @@ func (o *LoadBalancer) PrivateNetFor(network *Network) *LoadBalancerPrivateNet { // LoadBalancerClient is a client for the Load Balancers API. type LoadBalancerClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*LoadBalancer] } // GetByID retrieves a Load Balancer by its ID. If the Load Balancer does not exist, nil is returned. diff --git a/hcloud/network.go b/hcloud/network.go index 23a1ff71..0bf409e2 100644 --- a/hcloud/network.go +++ b/hcloud/network.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/url" + "strconv" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" @@ -56,6 +57,13 @@ type Network struct { ExposeRoutesToVSwitch bool } +func (o *Network) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // NetworkSubnet represents a subnet of a network in the Hetzner Cloud. type NetworkSubnet struct { Type NetworkSubnetType @@ -79,7 +87,7 @@ type NetworkProtection struct { // NetworkClient is a client for the network API. type NetworkClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*Network] } // GetByID retrieves a network by its ID. If the network does not exist, nil is returned. diff --git a/hcloud/primary_ip.go b/hcloud/primary_ip.go index a8d64eb7..19eb8bc5 100644 --- a/hcloud/primary_ip.go +++ b/hcloud/primary_ip.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/url" + "strconv" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" @@ -34,6 +35,13 @@ type PrimaryIP struct { Datacenter *Datacenter } +func (o *PrimaryIP) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // PrimaryIPProtection represents the protection level of a Primary IP. type PrimaryIPProtection struct { Delete bool @@ -154,7 +162,7 @@ type PrimaryIPChangeProtectionResult = schema.PrimaryIPActionChangeProtectionRes // PrimaryIPClient is a client for the Primary IP API. type PrimaryIPClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*PrimaryIP] } // GetByID retrieves a Primary IP by its ID. If the Primary IP does not exist, nil is returned. diff --git a/hcloud/server.go b/hcloud/server.go index 3192ed88..7e2eb7c9 100644 --- a/hcloud/server.go +++ b/hcloud/server.go @@ -44,6 +44,13 @@ type Server struct { Datacenter *Datacenter } +func (o *Server) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // ServerProtection represents the protection level of a server. type ServerProtection struct { Delete, Rebuild bool @@ -203,7 +210,7 @@ func (o *Server) PrivateNetFor(network *Network) *ServerPrivateNet { // ServerClient is a client for the servers API. type ServerClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*Server] } // GetByID retrieves a server by its ID. If the server does not exist, nil is returned. diff --git a/hcloud/storage_box.go b/hcloud/storage_box.go index e5b34299..338d194a 100644 --- a/hcloud/storage_box.go +++ b/hcloud/storage_box.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/url" + "strconv" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" @@ -30,6 +31,13 @@ type StorageBox struct { Created time.Time } +func (o *StorageBox) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + // StorageBoxAccessSettings represents the access settings of a [StorageBox]. type StorageBoxAccessSettings struct { ReachableExternally bool @@ -87,7 +95,7 @@ const ( // Experimental: [StorageBoxClient] is experimental, breaking changes may occur within minor releases. type StorageBoxClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*StorageBox] } // GetByID retrieves a [StorageBox] by its ID. If the [StorageBox] does not exist, nil is returned. diff --git a/hcloud/volume.go b/hcloud/volume.go index 074b22fa..1e2a34b7 100644 --- a/hcloud/volume.go +++ b/hcloud/volume.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/url" + "strconv" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil" @@ -25,6 +26,13 @@ type Volume struct { Created time.Time } +func (o *Volume) pathID() (string, error) { + if o.ID == 0 { + return "", missingField(o, "ID") + } + return strconv.FormatInt(o.ID, 10), nil +} + const ( VolumeFormatExt4 = "ext4" VolumeFormatXFS = "xfs" @@ -38,7 +46,7 @@ type VolumeProtection struct { // VolumeClient is a client for the volume API. type VolumeClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*Volume] } // VolumeStatus specifies a volume's status. diff --git a/hcloud/zone.go b/hcloud/zone.go index cb48c5a5..5d342fa0 100644 --- a/hcloud/zone.go +++ b/hcloud/zone.go @@ -108,13 +108,17 @@ func (o *Zone) idOrName() (string, error) { } } +func (o *Zone) pathID() (string, error) { + return o.idOrName() +} + // ZoneClient is a client for the Zone (DNS) API. // // See https://docs.hetzner.cloud/reference/cloud#zones and // https://docs.hetzner.cloud/reference/cloud#zone-rrsets. type ZoneClient struct { client *Client - Action *ResourceActionClient + Action *ResourceActionClient[*Zone] } // GetByID returns a single [Zone]. diff --git a/hcloud/zz_action_client_iface.go b/hcloud/zz_action_client_iface.go index ec4d3bfb..1ba03f50 100644 --- a/hcloud/zz_action_client_iface.go +++ b/hcloud/zz_action_client_iface.go @@ -10,7 +10,7 @@ import ( type IActionClient interface { // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. GetByID(ctx context.Context, id int64) (*Action, *Response, error) - // List returns a list of actions for a specific page. + // List returns a paginated list of actions. // // Please note that filters specified in opts are not taken into account // when their value corresponds to their zero value or when they are empty. diff --git a/hcloud/zz_resource_action_client_iface.go b/hcloud/zz_resource_action_client_iface.go index 67910ab2..8a848a74 100644 --- a/hcloud/zz_resource_action_client_iface.go +++ b/hcloud/zz_resource_action_client_iface.go @@ -7,14 +7,21 @@ import ( ) // IResourceActionClient ... -type IResourceActionClient interface { +type IResourceActionClient[R actionSupporter] interface { // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. GetByID(ctx context.Context, id int64) (*Action, *Response, error) - // List returns a list of actions for a specific page. + // List returns a paginated list of actions. // // Please note that filters specified in opts are not taken into account // when their value corresponds to their zero value or when they are empty. List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) // All returns all actions for the given options. All(ctx context.Context, opts ActionListOpts) ([]*Action, error) + // ListFor returns a paginated list of actions for the given Resource. + // + // Please note that filters specified in opts are not taken into account + // when their value corresponds to their zero value or when they are empty. + ListFor(ctx context.Context, resource R, opts ActionListOpts) ([]*Action, *Response, error) + // AllFor returns all actions for the given Resource. + AllFor(ctx context.Context, resource R, opts ActionListOpts) ([]*Action, error) }