From 199d111f449f9104de4a5c2165bbe17b4462b320 Mon Sep 17 00:00:00 2001 From: bbanucha Date: Mon, 2 Nov 2026 18:07:15 +0100 Subject: [PATCH] DPS-46305 Add 2 new log types for VPN Gateway --- monitor_log_streams.go | 29 +++- .../fixtures/monitor_log_stream_vpngw.json | 15 ++ test/unit/monitor_logs_test.go | 135 ++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 test/unit/fixtures/monitor_log_stream_vpngw.json diff --git a/monitor_log_streams.go b/monitor_log_streams.go index f9f9af700..78091bd4b 100644 --- a/monitor_log_streams.go +++ b/monitor_log_streams.go @@ -3,6 +3,7 @@ package linodego import ( "context" "encoding/json" + "errors" "time" "github.com/linode/linodego/v2/internal/parseabletime" @@ -16,6 +17,10 @@ const ( StreamTypeAuditLogs StreamType = "audit_logs" // StreamTypeLKEAuditLogs configures a stream for LKE enterprise cluster audit logs. StreamTypeLKEAuditLogs StreamType = "lke_audit_logs" + // StreamTypeVPNGWIPSecLogs configures a stream for VPN Gateway IPsec logs. + StreamTypeVPNGWIPSecLogs StreamType = "vpngw_ipsec_logs" + // StreamTypeVPNGWBGPLogs configures a stream for VPN Gateway BGP logs. + StreamTypeVPNGWBGPLogs StreamType = "vpngw_bgp_logs" ) // StreamStatus represents the availability state of an ACLP logs stream. @@ -43,10 +48,32 @@ const ( ) // StreamDetails contains additional details for a logs stream. -// This only applies to streams with a Type of StreamTypeLKEAuditLogs. +// Use ClusterIDs and IsAutoAddAllClustersEnabled for LKE audit logs, or +// VPNGatewayIDs for VPN Gateway IPsec and BGP logs. Do not mix these fields. type StreamDetails struct { ClusterIDs []int `json:"cluster_ids,omitzero"` IsAutoAddAllClustersEnabled bool `json:"is_auto_add_all_clusters_enabled"` + // VPNGatewayIDs selects unique, positive gateway IDs. The list must not be empty. + // Updating this list replaces all previously selected gateway IDs. + VPNGatewayIDs []int `json:"vpn_gateway_ids,omitzero"` +} + +// MarshalJSON omits LKE-specific fields for VPN Gateway details while preserving +// explicit false values for existing LKE configurations. +func (s StreamDetails) MarshalJSON() ([]byte, error) { + if s.VPNGatewayIDs != nil { + if s.ClusterIDs != nil || s.IsAutoAddAllClustersEnabled { + return nil, errors.New("VPN Gateway stream details cannot include LKE cluster settings") + } + + return json.Marshal(struct { + VPNGatewayIDs []int `json:"vpn_gateway_ids"` + }{VPNGatewayIDs: s.VPNGatewayIDs}) + } + + type mask StreamDetails + + return json.Marshal(mask(s)) } // StreamDestination is a destination configured on an ACLP logs stream. diff --git a/test/unit/fixtures/monitor_log_stream_vpngw.json b/test/unit/fixtures/monitor_log_stream_vpngw.json new file mode 100644 index 000000000..99929e21e --- /dev/null +++ b/test/unit/fixtures/monitor_log_stream_vpngw.json @@ -0,0 +1,15 @@ +{ + "id": 790, + "label": "VPN Gateway logs", + "type": "vpngw_ipsec_logs", + "status": "active", + "version": 1, + "destinations": [], + "details": { + "vpn_gateway_ids": [50, 60] + }, + "created": "2026-10-01T12:00:00", + "updated": "2026-10-01T12:00:00", + "created_by": "tester", + "updated_by": "tester" +} diff --git a/test/unit/monitor_logs_test.go b/test/unit/monitor_logs_test.go index f7b404b95..ba49d98a9 100644 --- a/test/unit/monitor_logs_test.go +++ b/test/unit/monitor_logs_test.go @@ -2,10 +2,14 @@ package unit import ( "context" + "encoding/json" + "net/http" "testing" + "github.com/jarcoal/httpmock" "github.com/linode/linodego/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -597,3 +601,134 @@ func TestListLogStreamQuotas(t *testing.T) { assert.Equal(t, 10, quotas[1].QuotaLimit) assert.Equal(t, "logs_streams_aclp_lke_audit_logs_streams", quotas[1].QuotaType) } + +func TestStreamDetailsMarshalJSON(t *testing.T) { + tests := []struct { + name string + details linodego.StreamDetails + want string + wantErr bool + }{ + { + name: "LKE explicit false", + details: linodego.StreamDetails{ClusterIDs: []int{50, 60}}, + want: `{"cluster_ids":[50,60],"is_auto_add_all_clusters_enabled":false}`, + }, + { + name: "LKE auto add", + details: linodego.StreamDetails{IsAutoAddAllClustersEnabled: true}, + want: `{"is_auto_add_all_clusters_enabled":true}`, + }, + { + name: "VPN", + details: linodego.StreamDetails{VPNGatewayIDs: []int{50, 60}}, + want: `{"vpn_gateway_ids":[50,60]}`, + }, + { + name: "VPN empty list remains explicit", + details: linodego.StreamDetails{VPNGatewayIDs: []int{}}, + want: `{"vpn_gateway_ids":[]}`, + }, + { + name: "mixed cluster IDs", + details: linodego.StreamDetails{VPNGatewayIDs: []int{50}, ClusterIDs: []int{60}}, + wantErr: true, + }, + { + name: "mixed auto add", + details: linodego.StreamDetails{VPNGatewayIDs: []int{50}, IsAutoAddAllClustersEnabled: true}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.details) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.JSONEq(t, tt.want, string(data)) + }) + } +} + +func TestLogStreamsVPNGateway(t *testing.T) { + for _, streamType := range []linodego.StreamType{ + linodego.StreamTypeVPNGWIPSecLogs, + linodego.StreamTypeVPNGWBGPLogs, + } { + t.Run(string(streamType), func(t *testing.T) { + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + fixture, err := fixtures.GetFixture("monitor_log_stream_vpngw") + require.NoError(t, err) + fixtureJSON, err := json.Marshal(fixture) + require.NoError(t, err) + var response map[string]interface{} + require.NoError(t, json.Unmarshal(fixtureJSON, &response)) + response["type"] = string(streamType) + + checkStream := func(stream *linodego.Stream) { + t.Helper() + require.NotNil(t, stream) + assert.Equal(t, streamType, stream.Type) + require.NotNil(t, stream.Details) + assert.Equal(t, []int{50, 60}, stream.Details.VPNGatewayIDs) + assert.Nil(t, stream.Details.ClusterIDs) + require.NotNil(t, stream.Created) + require.NotNil(t, stream.Updated) + } + for _, method := range []string{http.MethodPost, http.MethodPut} { + path := "monitor/streams" + if method == http.MethodPut { + path += "/790" + } + httpmock.RegisterResponder(method, base.BaseURL+path, func(req *http.Request) (*http.Response, error) { + var body map[string]json.RawMessage + require.NoError(t, json.NewDecoder(req.Body).Decode(&body)) + assert.JSONEq(t, `{"vpn_gateway_ids":[50,60]}`, string(body["details"])) + if req.Method == http.MethodPost { + assert.JSONEq(t, `"`+string(streamType)+`"`, string(body["type"])) + assert.JSONEq(t, `[1]`, string(body["destinations"])) + } else { + assert.NotContains(t, body, "type") + } + return httpmock.NewJsonResponse(http.StatusOK, response) + }) + } + details := &linodego.StreamDetails{VPNGatewayIDs: []int{50, 60}} + created, err := base.Client.CreateLogStream(context.Background(), linodego.StreamCreateOptions{ + Label: "VPN Gateway logs", Type: streamType, Destinations: []int{1}, Details: details, + }) + require.NoError(t, err) + checkStream(created) + + updated, err := base.Client.UpdateLogStream(context.Background(), 790, linodego.StreamUpdateOptions{Details: details}) + require.NoError(t, err) + checkStream(updated) + + base.MockGet("monitor/streams/790", response) + stream, err := base.Client.GetLogStream(context.Background(), 790) + require.NoError(t, err) + checkStream(stream) + + page := map[string]interface{}{ + "data": []interface{}{response}, "page": 1, "pages": 1, "results": 1, + } + base.MockGet("monitor/streams", page) + streams, err := base.Client.ListLogStreams(context.Background(), nil) + require.NoError(t, err) + require.Len(t, streams, 1) + checkStream(&streams[0]) + + base.MockGet("monitor/streams/790/history", page) + history, err := base.Client.ListLogStreamHistory(context.Background(), 790, nil) + require.NoError(t, err) + require.Len(t, history, 1) + checkStream(&history[0]) + }) + } +}