Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
35b2878
build(deps): bump golang.org/x/oauth2 from 0.35.0 to 0.36.0 (#909)
dependabot[bot] Mar 13, 2026
cf20b58
TPT-4298: Added PR title checking to lint workflow and new clean up r…
ezilber-akamai Mar 17, 2026
76d9391
TPT-4014: Redact sensitive data from logging (#906)
dawiddzhafarov Mar 20, 2026
802f57f
TPT-4277 Implement support for Reserved IP for IPv4
mgwoj Mar 23, 2026
3d04bdc
Update network_reserved_ips.go
mgwoj Mar 23, 2026
2264a2e
TPT-4277 Implement support for Reserved IP for IPv4
mgwoj Mar 23, 2026
dc00ea0
TPT-4320: Update FirewallID to use single pointer in LinodeInterfaceC…
zliang-akamai Mar 24, 2026
245707c
build(deps): bump slackapi/slack-github-action from 2.1.1 to 3.0.1 (#…
dependabot[bot] Mar 25, 2026
4a2a3ed
build(deps): bump actions/github-script from 7 to 8 (#918)
dependabot[bot] Mar 25, 2026
492a403
TPT-4277 Implement support for Reserved IP for IPv4
mgwoj Mar 26, 2026
0606e68
TPT-4277 Implement support for Reserved IP for IPv4
mgwoj Mar 26, 2026
fb8c404
TPT-4318: Add @linode/dx-sdets to CODEOWNERS (#920)
lgarber-akamai Mar 26, 2026
69df217
TPT-3807: Added `DiskEncryption` field for LKE Node Pool creation (#917)
ezilber-akamai Mar 27, 2026
6a5e955
build(deps): bump golang.org/x/net from 0.51.0 to 0.52.0 (#911)
dependabot[bot] Mar 27, 2026
2b51e3c
TPT-4234: Fix firewall device for linode interfaces and add entities …
zliang-akamai Mar 27, 2026
09803f2
Cleanup LA notices for block storage encryption (#902)
zliang-akamai Mar 27, 2026
04c1c9e
Remove content field from list alert channels response (#925)
shkaruna Apr 8, 2026
ad25985
feat: add ACLP list entities method (#923)
shkaruna Apr 9, 2026
f1eb655
Merge branch 'linode:main' into feature/TPT-4277-linodego-implement-s…
mgwoj Apr 15, 2026
e150bbf
TPT-4277: Implement support for Reserved IP for IPv4
mgwoj Apr 15, 2026
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
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,35 @@ on:
jobs:
lint-tidy:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
# Enforce TPT-1234: prefix on PR titles, with the following exemptions:
# - PRs labeled 'dependencies' (e.g. Dependabot PRs)
# - PRs labeled 'hotfix' (urgent fixes that may not have a ticket)
# - PRs labeled 'community-contribution' (external contributors without TPT tickets)
# - PRs labeled 'ignore-for-release' (release PRs that don't need a ticket prefix)
- name: Validate PR Title
if: github.event_name == 'pull_request'
uses: amannn/action-semantic-pull-request@v6
with:
types: |
TPT-\d+
requireScope: false
# Override the default header pattern to allow hyphens and digits in the type
# (e.g. "TPT-4298: Description"). The default pattern only matches word
# characters (\w) which excludes hyphens.
headerPattern: '^([\w-]+):\s?(.*)$'
headerPatternCorrespondence: type, subject
ignoreLabels: |
dependencies
hotfix
community-contribution
ignore-for-release
env:
GITHUB_TOKEN: ${{ github.token }}

- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
Expand Down Expand Up @@ -99,7 +127,7 @@ jobs:

steps:
- name: Notify Slack
uses: slackapi/slack-github-action@v2.1.1
uses: slackapi/slack-github-action@v3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/clean-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Clean Release Notes

on:
release:
types: [published]

jobs:
clean-release-notes:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Remove ticket prefixes from release notes
uses: actions/github-script@v8
with:
Comment thread
mgwoj marked this conversation as resolved.
script: |
const release = context.payload.release;

let body = release.body;

if (!body) {
console.log("Release body empty, nothing to clean.");
return;
}

// Remove ticket prefixes like "TPT-1234: " or "TPT-1234:"
body = body.replace(/TPT-\d+:\s*/g, '');

await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.id,
body: body
});

console.log("Release notes cleaned.");
2 changes: 1 addition & 1 deletion .github/workflows/nightly_smoke_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

- name: Notify Slack
if: (success() || failure()) && github.repository == 'linode/linodego'
uses: slackapi/slack-github-action@v2.1.1
uses: slackapi/slack-github-action@v3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-notify-slack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify Slack - Main Message
uses: slackapi/slack-github-action@v2.1.1
uses: slackapi/slack-github-action@v3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @linode/dx
* @linode/dx @linode/dx-sdets
39 changes: 35 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ Body: {{.Body}}`))

var envDebug = false

// redactHeadersMap is a map of headers that should be redacted in logs,
// mapping the header name to its redacted value.
var redactHeadersMap = map[string]string{
"Authorization": "Bearer *******************************",
}

// Client is a wrapper around the Resty client
type Client struct {
resty *resty.Client
Expand Down Expand Up @@ -394,6 +400,19 @@ func (c *httpClient) applyAfterResponse(resp *http.Response) error {
return nil
}

// nolint:unused
func redactHeaders(headers http.Header) http.Header {
redacted := headers.Clone()

for header, redactedValue := range redactHeadersMap {
if headers.Get(header) != "" {
redacted.Set(header, redactedValue)
}
}

return redacted
}

// nolint:unused
func (c *httpClient) logRequest(req *http.Request, method, url string, bodyBuffer *bytes.Buffer) {
var reqBody string
Expand All @@ -408,7 +427,7 @@ func (c *httpClient) logRequest(req *http.Request, method, url string, bodyBuffe
err := reqLogTemplate.Execute(&logBuf, map[string]any{
"Method": method,
"URL": url,
"Headers": req.Header,
"Headers": redactHeaders(req.Header),
"Body": reqBody,
})
if err == nil {
Expand Down Expand Up @@ -456,7 +475,7 @@ func (c *httpClient) logResponse(resp *http.Response) (*http.Response, error) {

err := respLogTemplate.Execute(&logBuf, map[string]any{
"Status": resp.Status,
"Headers": resp.Header,
"Headers": redactHeaders(resp.Header),
"Body": respBody.String(),
})
if err == nil {
Expand Down Expand Up @@ -827,10 +846,22 @@ func (c *Client) updateHostURL() {
)
}

func redactLogHeaders(header http.Header) {
for h, redactedValue := range redactHeadersMap {
if header.Get(h) != "" {
header.Set(h, redactedValue)
}
}
}

func (c *Client) enableLogSanitization() *Client {
c.resty.OnRequestLog(func(r *resty.RequestLog) error {
// masking authorization header
r.Header.Set("Authorization", "Bearer *******************************")
redactLogHeaders(r.Header)
return nil
})

c.resty.OnResponseLog(func(r *resty.ResponseLog) error {
redactLogHeaders(r.Header)
return nil
})

Expand Down
99 changes: 99 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/jarcoal/httpmock"
"github.com/linode/linodego/internal/testutil"
"github.com/stretchr/testify/require"
)

func TestClient_SetAPIVersion(t *testing.T) {
Expand Down Expand Up @@ -703,3 +704,101 @@ func TestMonitorClient_SetAPIBasics(t *testing.T) {
t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost))
}
}

func TestRedactHeaders(t *testing.T) {
tests := []struct {
name string
headers http.Header
wantVal map[string]string
}{
{
name: "redacts authorization header",
headers: http.Header{
"Authorization": []string{"Bearer supersecrettoken"},
"Content-Type": []string{"application/json"},
},
wantVal: map[string]string{
"Authorization": redactHeadersMap["Authorization"],
"Content-Type": "application/json",
},
},
{
name: "leaves non-sensitive headers unchanged",
headers: http.Header{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
},
wantVal: map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
},
},
{
name: "handles empty headers",
headers: http.Header{},
wantVal: map[string]string{},
},
{
name: "does not mutate original headers",
headers: http.Header{
"Authorization": []string{"Bearer supersecrettoken"},
},
wantVal: map[string]string{
"Authorization": redactHeadersMap["Authorization"],
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originalAuth := tt.headers.Get("Authorization")

result := redactHeaders(tt.headers)

// Verify expected values in result
for key, expectedVal := range tt.wantVal {
if got := result.Get(key); got != expectedVal {
t.Errorf("redactHeaders() header %q = %q, want %q", key, got, expectedVal)
}
}

// Verify original was not mutated
if tt.headers.Get("Authorization") != originalAuth {
t.Error("redactHeaders() mutated the original headers")
}
})
}
}

func TestEnableLogSanitization(t *testing.T) {
mockClient := testutil.CreateMockClient(t, NewClient)
mockClient.SetDebug(true)

plainTextToken := "supersecrettoken"
mockClient.SetToken(plainTextToken)

var logBuf bytes.Buffer
logger := testutil.CreateLogger()
logger.L.SetOutput(&logBuf)
mockClient.SetLogger(logger)

httpmock.RegisterResponder("GET", "=~.*",
httpmock.NewStringResponder(200, `{}`).HeaderSet(http.Header{
"Authorization": []string{"Bearer " + plainTextToken},
}))

_, err := mockClient.resty.R().Get("https://api.linode.com/v4/test")
require.NoError(t, err)

logOutput := logBuf.String()

// Verify token is not present in either request or response logs
if strings.Contains(logOutput, plainTextToken) {
t.Errorf("log output contains raw token %q, expected it to be redacted", plainTextToken)
}

// Verify Authorization header still appears (as redacted value) in request log
if !strings.Contains(logOutput, "Authorization") {
t.Error("expected Authorization header to appear in request log output")
}
}
9 changes: 5 additions & 4 deletions firewall_devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ func (device *FirewallDevice) UnmarshalJSON(b []byte) error {

// FirewallDeviceEntity contains information about a device associated with a Firewall
type FirewallDeviceEntity struct {
ID int `json:"id"`
Type FirewallDeviceType `json:"type"`
Label string `json:"label"`
URL string `json:"url"`
ID int `json:"id"`
Type FirewallDeviceType `json:"type"`
Label string `json:"label"`
URL string `json:"url"`
ParentEntity *FirewallDeviceEntity `json:"parent_entity"`
}

// ListFirewallDevices get devices associated with a given Firewall
Expand Down
21 changes: 11 additions & 10 deletions firewalls.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ const (

// A Firewall is a set of networking rules (iptables) applied to Devices with which it is associated
type Firewall struct {
ID int `json:"id"`
Label string `json:"label"`
Status FirewallStatus `json:"status"`
Tags []string `json:"tags,omitempty"`
Rules FirewallRuleSet `json:"rules"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`
ID int `json:"id"`
Label string `json:"label"`
Status FirewallStatus `json:"status"`
Tags []string `json:"tags"`
Rules FirewallRuleSet `json:"rules"`
Entities []FirewallDeviceEntity `json:"entities"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`
}

// DevicesCreationOptions fields are used when adding devices during the Firewall creation process.
type DevicesCreationOptions struct {
Linodes []int `json:"linodes,omitempty"`
NodeBalancers []int `json:"nodebalancers,omitempty"`
Interfaces []int `json:"interfaces,omitempty"`
Linodes []int `json:"linodes,omitempty"`
NodeBalancers []int `json:"nodebalancers,omitempty"`
LinodeInterfaces []int `json:"linode_interfaces,omitempty"`
}

// FirewallCreateOptions fields are those accepted by CreateFirewall
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/go-querystring v1.2.0
github.com/jarcoal/httpmock v1.4.1
golang.org/x/net v0.51.0
golang.org/x/oauth2 v0.35.0
golang.org/x/text v0.34.0
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/text v0.35.0
gopkg.in/ini.v1 v1.67.1
)

Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
16 changes: 7 additions & 9 deletions instance_disks.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ import (

// InstanceDisk represents an Instance Disk object
type InstanceDisk struct {
ID int `json:"id"`
Label string `json:"label"`
Status DiskStatus `json:"status"`
Size int `json:"size"`
Filesystem DiskFilesystem `json:"filesystem"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`

// NOTE: Disk encryption may not currently be available to all users.
ID int `json:"id"`
Label string `json:"label"`
Status DiskStatus `json:"status"`
Size int `json:"size"`
Filesystem DiskFilesystem `json:"filesystem"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`
DiskEncryption InstanceDiskEncryption `json:"disk_encryption"`
}

Expand Down
Loading
Loading