Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions instance_ips.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ type InstanceIPv4Response struct {

// InstanceIP represents an Instance IP with additional DNS and networking details
type InstanceIP struct {
Address string `json:"address"`
Gateway string `json:"gateway"`
SubnetMask string `json:"subnet_mask"`
Prefix int `json:"prefix"`
Type InstanceIPType `json:"type"`
Public bool `json:"public"`
RDNS string `json:"rdns"`
LinodeID int `json:"linode_id"`
InterfaceID *int `json:"interface_id"`
Region string `json:"region"`
VPCNAT1To1 *InstanceIPNAT1To1 `json:"vpc_nat_1_1"`
Reserved bool `json:"reserved"`
Address string `json:"address"`
Gateway string `json:"gateway"`
SubnetMask string `json:"subnet_mask"`
Prefix int `json:"prefix"`
Type InstanceIPType `json:"type"`
Public bool `json:"public"`
RDNS string `json:"rdns"`
LinodeID int `json:"linode_id"`
InterfaceID *int `json:"interface_id"`
Region string `json:"region"`
VPCNAT1To1 *InstanceIPNAT1To1 `json:"vpc_nat_1_1"`
Reserved bool `json:"reserved"`
Tags []string `json:"tags"`
AssignedEntity *ReservedIPAssignedEntity `json:"assigned_entity"`
Comment thread
mgwoj marked this conversation as resolved.
}
Comment thread
mgwoj marked this conversation as resolved.

// VPCIP represents a private IP address in a VPC subnet with additional networking details
Expand Down
43 changes: 42 additions & 1 deletion network_reserved_ips.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,40 @@ import (
"context"
)

// ReservedIPAssignedEntity represents the entity that a reserved IP is assigned to.
// NOTE: Reserved IP feature may not currently be available to all users.
type ReservedIPAssignedEntity struct {
ID int `json:"id"`
Label string `json:"label"`
Type string `json:"type"`
URL string `json:"url"`
}

// ReserveIPOptions represents the options for reserving an IP address
// NOTE: Reserved IP feature may not currently be available to all users.
type ReserveIPOptions struct {
Region string `json:"region"`
Region string `json:"region"`
Tags []string `json:"tags,omitempty"`
}

// UpdateReservedIPOptions represents the options for updating a reserved IP address
// NOTE: Reserved IP feature may not currently be available to all users.
type UpdateReservedIPOptions struct {
Tags []string `json:"tags,omitzero"`
}
Comment on lines +23 to 27
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpdateReservedIPOptions.Tags is a non-pointer slice without omitempty, so the zero value (nil) will marshal as "tags":null. If the API expects an array (even when clearing), this can cause request validation errors or unintended semantics. Consider using Tags *[]string json:"tags,omitempty"`` (consistent with other update options in the SDK) or validate that Tags is non-nil before issuing the PUT.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@yec-akamai yec-akamai Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use omitzero instead of omitempty for any list to make sure empty slice are correctly marshalled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this is the way to go


// ReservedIPPrice represents the pricing information for a reserved IP type.
// It is an alias of the shared baseTypePrice to keep pricing consistent across resources.
type ReservedIPPrice = baseTypePrice

// ReservedIPRegionPrice represents region-specific pricing for a reserved IP type.
// It is an alias of the shared baseTypeRegionPrice to keep region pricing consistent across resources.
type ReservedIPRegionPrice = baseTypeRegionPrice

// ReservedIPType represents a reserved IP type with pricing information.
// It reuses the generic baseType to avoid duplicating type/pricing structures.
type ReservedIPType = baseType[ReservedIPPrice, ReservedIPRegionPrice]

// ListReservedIPAddresses retrieves a list of reserved IP addresses
// NOTE: Reserved IP feature may not currently be available to all users.
func (c *Client) ListReservedIPAddresses(ctx context.Context, opts *ListOptions) ([]InstanceIP, error) {
Expand All @@ -30,9 +58,22 @@ func (c *Client) ReserveIPAddress(ctx context.Context, opts ReserveIPOptions) (*
return doPOSTRequest[InstanceIP](ctx, c, "networking/reserved/ips", opts)
}

// UpdateReservedIPAddress updates the tags of a reserved IP address
// NOTE: Reserved IP feature may not currently be available to all users.
func (c *Client) UpdateReservedIPAddress(ctx context.Context, address string, opts UpdateReservedIPOptions) (*InstanceIP, error) {
e := formatAPIPath("networking/reserved/ips/%s", address)
return doPUTRequest[InstanceIP](ctx, c, e, opts)
}

// DeleteReservedIPAddress deletes a reserved IP address
// NOTE: Reserved IP feature may not currently be available to all users.
func (c *Client) DeleteReservedIPAddress(ctx context.Context, ipAddress string) error {
e := formatAPIPath("networking/reserved/ips/%s", ipAddress)
return doDELETERequest(ctx, c, e)
}

// ListReservedIPTypes retrieves a list of reserved IP types with pricing information
// NOTE: Reserved IP feature may not currently be available to all users.
func (c *Client) ListReservedIPTypes(ctx context.Context, opts *ListOptions) ([]ReservedIPType, error) {
return getPaginatedResults[ReservedIPType](ctx, c, "networking/reserved/ips/types", opts)
}
16 changes: 12 additions & 4 deletions tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ type TagCreateOptions struct {
Label string `json:"label"`
Linodes []int `json:"linodes,omitempty"`
// @TODO is this implemented?
LKEClusters []int `json:"lke_clusters,omitempty"`
Domains []int `json:"domains,omitempty"`
Volumes []int `json:"volumes,omitempty"`
NodeBalancers []int `json:"nodebalancers,omitempty"`
LKEClusters []int `json:"lke_clusters,omitempty"`
Domains []int `json:"domains,omitempty"`
Volumes []int `json:"volumes,omitempty"`
NodeBalancers []int `json:"nodebalancers,omitempty"`
ReservedIPv4Addresses []string `json:"reserved_ipv4_addresses,omitempty"`
}

// GetCreateOptions converts a Tag to TagCreateOptions for use in CreateTag
Expand Down Expand Up @@ -92,6 +93,13 @@ func (i *TaggedObject) fixData() (*TaggedObject, error) {
return nil, err
}

i.Data = obj
case "reserved_ipv4_address":
obj := InstanceIP{}
if err := json.Unmarshal(i.RawData, &obj); err != nil {
return nil, err
}

i.Data = obj
}

Expand Down
167 changes: 167 additions & 0 deletions test/integration/fixtures/TestReservedIPAddress_ReserveWithTags.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
version: 1
interactions:
- request:
body: '{"region":"us-east","tags":["lb"]}'
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- linodego/dev https://github.com/linode/linodego
url: https://api.linode.com/v4beta/networking/reserved/ips
method: POST
response:
body: '{"address": "66.175.209.100", "gateway": "66.175.209.1", "subnet_mask": "255.255.255.0",
"prefix": 24, "type": "ipv4", "public": true, "rdns": "66-175-209-100.ip.linodeusercontent.com",
"linode_id": null, "interface_id": null, "region": "us-east", "vpc_nat_1_1":
null, "reserved": true, "tags": ["lb"], "assigned_entity": null}'
headers:
Access-Control-Allow-Credentials:
- "true"
Access-Control-Allow-Headers:
- Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter
Access-Control-Allow-Methods:
- HEAD, GET, OPTIONS, POST, PUT, DELETE
Access-Control-Allow-Origin:
- '*'
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Akamai-Internal-Account:
- '*'
Cache-Control:
- max-age=0, no-cache, no-store
Connection:
- keep-alive
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Pragma:
- no-cache
Strict-Transport-Security:
- max-age=31536000
Vary:
- Authorization, X-Filter
X-Accepted-Oauth-Scopes:
- ips:read_write
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "800"
status: 200 OK
code: 200
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- linodego/dev https://github.com/linode/linodego
url: https://api.linode.com/v4beta/networking/reserved/ips/66.175.209.100
method: GET
response:
body: '{"address": "66.175.209.100", "gateway": "66.175.209.1", "subnet_mask": "255.255.255.0",
"prefix": 24, "type": "ipv4", "public": true, "rdns": "66-175-209-100.ip.linodeusercontent.com",
"linode_id": null, "interface_id": null, "region": "us-east", "vpc_nat_1_1":
null, "reserved": true, "tags": ["lb"], "assigned_entity": null}'
headers:
Access-Control-Allow-Credentials:
- "true"
Access-Control-Allow-Headers:
- Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter
Access-Control-Allow-Methods:
- HEAD, GET, OPTIONS, POST, PUT, DELETE
Access-Control-Allow-Origin:
- '*'
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Akamai-Internal-Account:
- '*'
Cache-Control:
- max-age=0, no-cache, no-store
Connection:
- keep-alive
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Pragma:
- no-cache
Strict-Transport-Security:
- max-age=31536000
Vary:
- Authorization, X-Filter
X-Accepted-Oauth-Scopes:
- ips:read_only
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "800"
status: 200 OK
code: 200
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- linodego/dev https://github.com/linode/linodego
url: https://api.linode.com/v4beta/networking/reserved/ips/66.175.209.100
method: DELETE
response:
body: '{}'
headers:
Access-Control-Allow-Credentials:
- "true"
Access-Control-Allow-Headers:
- Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter
Access-Control-Allow-Methods:
- HEAD, GET, OPTIONS, POST, PUT, DELETE
Access-Control-Allow-Origin:
- '*'
Akamai-Internal-Account:
- '*'
Cache-Control:
- max-age=0, no-cache, no-store
Connection:
- keep-alive
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Pragma:
- no-cache
Strict-Transport-Security:
- max-age=31536000
X-Accepted-Oauth-Scopes:
- ips:read_write
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "800"
status: 200 OK
code: 200
duration: ""
Loading
Loading