Skip to content

Commit fd808ca

Browse files
Lionel-Wilsonactions-user
authored andcommitted
Implement NAT Gateways Client and Adapter for Azure (#4169)
<img width="1488" height="1007" alt="image" src="https://github.com/user-attachments/assets/eb8b7cc0-39a8-4c49-96af-e1fad20f15dc" /> <!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Medium risk because it adds a new Azure SDK client and adapter into the main `Adapters()` initialization path, increasing API surface/calls and requiring correct scope/link parsing. Changes are additive and covered by unit tests/mocks. > > **Overview** > **Adds discovery for Azure NAT Gateways.** Introduces `clients.NatGatewaysClient` (with list pager support) plus a generated GoMock, and registers a new `NewNetworkNatGateway` wrapper/adapter. > > The NAT gateway wrapper supports `Get`, `List`, and `ListStream`, maps provisioning state to item health, and emits linked-item queries to related `PublicIPAddress`, `PublicIPPrefix`, `Subnet`, and `VirtualNetwork` resources; `manual/adapters.go` now initializes `armnetwork.NewNatGatewaysClient` and includes the adapter in both real and placeholder adapter lists, with dedicated unit tests validating get/list behavior and link generation. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d470996177d2dd3f107bcdd78f0549ca2e2bd3dd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> GitOrigin-RevId: 968c00193a187dfd2fcd95ecf245cac53afb2c20
1 parent 35863ff commit fd808ca

5 files changed

Lines changed: 754 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package clients
2+
3+
import (
4+
"context"
5+
6+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9"
7+
)
8+
9+
//go:generate mockgen -destination=../shared/mocks/mock_nat_gateways_client.go -package=mocks -source=nat-gateways-client.go
10+
11+
// NatGatewaysPager is a type alias for the generic Pager interface with NAT gateway list response type.
12+
type NatGatewaysPager = Pager[armnetwork.NatGatewaysClientListResponse]
13+
14+
// NatGatewaysClient is an interface for interacting with Azure NAT gateways.
15+
type NatGatewaysClient interface {
16+
Get(ctx context.Context, resourceGroupName string, natGatewayName string, options *armnetwork.NatGatewaysClientGetOptions) (armnetwork.NatGatewaysClientGetResponse, error)
17+
NewListPager(resourceGroupName string, options *armnetwork.NatGatewaysClientListOptions) NatGatewaysPager
18+
}
19+
20+
type natGatewaysClient struct {
21+
client *armnetwork.NatGatewaysClient
22+
}
23+
24+
func (c *natGatewaysClient) Get(ctx context.Context, resourceGroupName string, natGatewayName string, options *armnetwork.NatGatewaysClientGetOptions) (armnetwork.NatGatewaysClientGetResponse, error) {
25+
return c.client.Get(ctx, resourceGroupName, natGatewayName, options)
26+
}
27+
28+
func (c *natGatewaysClient) NewListPager(resourceGroupName string, options *armnetwork.NatGatewaysClientListOptions) NatGatewaysPager {
29+
return c.client.NewListPager(resourceGroupName, options)
30+
}
31+
32+
// NewNatGatewaysClient creates a new NatGatewaysClient from the Azure SDK client.
33+
func NewNatGatewaysClient(client *armnetwork.NatGatewaysClient) NatGatewaysClient {
34+
return &natGatewaysClient{client: client}
35+
}

sources/azure/manual/adapters.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred
251251
return nil, fmt.Errorf("failed to create virtual network gateways client: %w", err)
252252
}
253253

254+
natGatewaysClient, err := armnetwork.NewNatGatewaysClient(subscriptionID, cred, nil)
255+
if err != nil {
256+
return nil, fmt.Errorf("failed to create nat gateways client: %w", err)
257+
}
258+
254259
managedHSMsClient, err := armkeyvault.NewManagedHsmsClient(subscriptionID, cred, nil)
255260
if err != nil {
256261
return nil, fmt.Errorf("failed to create managed hsms client: %w", err)
@@ -591,6 +596,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred
591596
clients.NewVirtualNetworkGatewaysClient(virtualNetworkGatewaysClient),
592597
resourceGroupScopes,
593598
), cache),
599+
sources.WrapperToAdapter(NewNetworkNatGateway(
600+
clients.NewNatGatewaysClient(natGatewaysClient),
601+
resourceGroupScopes,
602+
), cache),
594603
sources.WrapperToAdapter(NewSqlServer(
595604
clients.NewSqlServersClient(sqlServersClient),
596605
resourceGroupScopes,
@@ -747,6 +756,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred
747756
sources.WrapperToAdapter(NewNetworkRouteTable(nil, placeholderResourceGroupScopes), noOpCache),
748757
sources.WrapperToAdapter(NewNetworkApplicationGateway(nil, placeholderResourceGroupScopes), noOpCache),
749758
sources.WrapperToAdapter(NewNetworkVirtualNetworkGateway(nil, placeholderResourceGroupScopes), noOpCache),
759+
sources.WrapperToAdapter(NewNetworkNatGateway(nil, placeholderResourceGroupScopes), noOpCache),
750760
sources.WrapperToAdapter(NewSqlServer(nil, placeholderResourceGroupScopes), noOpCache),
751761
sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServer(nil, placeholderResourceGroupScopes), noOpCache),
752762
sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache),
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package manual
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9"
8+
"github.com/overmindtech/cli/go/discovery"
9+
"github.com/overmindtech/cli/go/sdp-go"
10+
"github.com/overmindtech/cli/go/sdpcache"
11+
"github.com/overmindtech/cli/sources"
12+
"github.com/overmindtech/cli/sources/azure/clients"
13+
azureshared "github.com/overmindtech/cli/sources/azure/shared"
14+
"github.com/overmindtech/cli/sources/shared"
15+
)
16+
17+
var NetworkNatGatewayLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNatGateway)
18+
19+
type networkNatGatewayWrapper struct {
20+
client clients.NatGatewaysClient
21+
22+
*azureshared.MultiResourceGroupBase
23+
}
24+
25+
// NewNetworkNatGateway creates a new networkNatGatewayWrapper instance.
26+
func NewNetworkNatGateway(client clients.NatGatewaysClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper {
27+
return &networkNatGatewayWrapper{
28+
client: client,
29+
MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase(
30+
resourceGroupScopes,
31+
sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK,
32+
azureshared.NetworkNatGateway,
33+
),
34+
}
35+
}
36+
37+
func (n networkNatGatewayWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) {
38+
rgScope, err := n.ResourceGroupScopeFromScope(scope)
39+
if err != nil {
40+
return nil, azureshared.QueryError(err, scope, n.Type())
41+
}
42+
pager := n.client.NewListPager(rgScope.ResourceGroup, nil)
43+
44+
var items []*sdp.Item
45+
for pager.More() {
46+
page, err := pager.NextPage(ctx)
47+
if err != nil {
48+
return nil, azureshared.QueryError(err, scope, n.Type())
49+
}
50+
51+
for _, ng := range page.Value {
52+
if ng.Name == nil {
53+
continue
54+
}
55+
item, sdpErr := n.azureNatGatewayToSDPItem(ng, scope)
56+
if sdpErr != nil {
57+
return nil, sdpErr
58+
}
59+
items = append(items, item)
60+
}
61+
}
62+
63+
return items, nil
64+
}
65+
66+
func (n networkNatGatewayWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) {
67+
rgScope, err := n.ResourceGroupScopeFromScope(scope)
68+
if err != nil {
69+
stream.SendError(azureshared.QueryError(err, scope, n.Type()))
70+
return
71+
}
72+
pager := n.client.NewListPager(rgScope.ResourceGroup, nil)
73+
for pager.More() {
74+
page, err := pager.NextPage(ctx)
75+
if err != nil {
76+
stream.SendError(azureshared.QueryError(err, scope, n.Type()))
77+
return
78+
}
79+
80+
for _, ng := range page.Value {
81+
if ng.Name == nil {
82+
continue
83+
}
84+
item, sdpErr := n.azureNatGatewayToSDPItem(ng, scope)
85+
if sdpErr != nil {
86+
stream.SendError(sdpErr)
87+
continue
88+
}
89+
cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey)
90+
stream.SendItem(item)
91+
}
92+
}
93+
}
94+
95+
func (n networkNatGatewayWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) {
96+
if len(queryParts) < 1 {
97+
return nil, &sdp.QueryError{
98+
ErrorType: sdp.QueryError_OTHER,
99+
ErrorString: "Get requires 1 query part: natGatewayName",
100+
Scope: scope,
101+
ItemType: n.Type(),
102+
}
103+
}
104+
105+
natGatewayName := queryParts[0]
106+
107+
rgScope, err := n.ResourceGroupScopeFromScope(scope)
108+
if err != nil {
109+
return nil, azureshared.QueryError(err, scope, n.Type())
110+
}
111+
resp, err := n.client.Get(ctx, rgScope.ResourceGroup, natGatewayName, nil)
112+
if err != nil {
113+
return nil, azureshared.QueryError(err, scope, n.Type())
114+
}
115+
116+
return n.azureNatGatewayToSDPItem(&resp.NatGateway, scope)
117+
}
118+
119+
func (n networkNatGatewayWrapper) azureNatGatewayToSDPItem(ng *armnetwork.NatGateway, scope string) (*sdp.Item, *sdp.QueryError) {
120+
attributes, err := shared.ToAttributesWithExclude(ng, "tags")
121+
if err != nil {
122+
return nil, azureshared.QueryError(err, scope, n.Type())
123+
}
124+
125+
if ng.Name == nil {
126+
return nil, azureshared.QueryError(errors.New("nat gateway name is nil"), scope, n.Type())
127+
}
128+
129+
sdpItem := &sdp.Item{
130+
Type: azureshared.NetworkNatGateway.String(),
131+
UniqueAttribute: "name",
132+
Attributes: attributes,
133+
Scope: scope,
134+
Tags: azureshared.ConvertAzureTags(ng.Tags),
135+
LinkedItemQueries: []*sdp.LinkedItemQuery{},
136+
}
137+
138+
// Health from provisioning state
139+
if ng.Properties != nil && ng.Properties.ProvisioningState != nil {
140+
switch *ng.Properties.ProvisioningState {
141+
case armnetwork.ProvisioningStateSucceeded:
142+
sdpItem.Health = sdp.Health_HEALTH_OK.Enum()
143+
case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting:
144+
sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum()
145+
case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled:
146+
sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum()
147+
default:
148+
sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum()
149+
}
150+
}
151+
152+
// Linked resources from Properties
153+
if ng.Properties == nil {
154+
return sdpItem, nil
155+
}
156+
props := ng.Properties
157+
158+
// Public IP addresses (V4 and V6)
159+
for _, refs := range [][]*armnetwork.SubResource{props.PublicIPAddresses, props.PublicIPAddressesV6} {
160+
for _, ref := range refs {
161+
if ref != nil && ref.ID != nil {
162+
refID := *ref.ID
163+
refName := azureshared.ExtractResourceName(refID)
164+
if refName != "" {
165+
linkedScope := azureshared.ExtractScopeFromResourceID(refID)
166+
if linkedScope == "" {
167+
linkedScope = scope
168+
}
169+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
170+
Query: &sdp.Query{
171+
Type: azureshared.NetworkPublicIPAddress.String(),
172+
Method: sdp.QueryMethod_GET,
173+
Query: refName,
174+
Scope: linkedScope,
175+
},
176+
})
177+
}
178+
}
179+
}
180+
}
181+
182+
// Public IP prefixes (V4 and V6)
183+
for _, refs := range [][]*armnetwork.SubResource{props.PublicIPPrefixes, props.PublicIPPrefixesV6} {
184+
for _, ref := range refs {
185+
if ref != nil && ref.ID != nil {
186+
refID := *ref.ID
187+
refName := azureshared.ExtractResourceName(refID)
188+
if refName != "" {
189+
linkedScope := azureshared.ExtractScopeFromResourceID(refID)
190+
if linkedScope == "" {
191+
linkedScope = scope
192+
}
193+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
194+
Query: &sdp.Query{
195+
Type: azureshared.NetworkPublicIPPrefix.String(),
196+
Method: sdp.QueryMethod_GET,
197+
Query: refName,
198+
Scope: linkedScope,
199+
},
200+
})
201+
}
202+
}
203+
}
204+
}
205+
206+
// Subnets (read-only references: subnets using this NAT gateway)
207+
for _, ref := range props.Subnets {
208+
if ref != nil && ref.ID != nil {
209+
subnetID := *ref.ID
210+
params := azureshared.ExtractPathParamsFromResourceID(subnetID, []string{"virtualNetworks", "subnets"})
211+
if len(params) >= 2 && params[0] != "" && params[1] != "" {
212+
linkedScope := azureshared.ExtractScopeFromResourceID(subnetID)
213+
if linkedScope == "" {
214+
linkedScope = scope
215+
}
216+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
217+
Query: &sdp.Query{
218+
Type: azureshared.NetworkSubnet.String(),
219+
Method: sdp.QueryMethod_GET,
220+
Scope: linkedScope,
221+
Query: shared.CompositeLookupKey(params[0], params[1]),
222+
},
223+
})
224+
}
225+
}
226+
}
227+
228+
// Source virtual network
229+
if props.SourceVirtualNetwork != nil && props.SourceVirtualNetwork.ID != nil {
230+
vnetID := *props.SourceVirtualNetwork.ID
231+
vnetName := azureshared.ExtractResourceName(vnetID)
232+
if vnetName != "" {
233+
linkedScope := azureshared.ExtractScopeFromResourceID(vnetID)
234+
if linkedScope == "" {
235+
linkedScope = scope
236+
}
237+
sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{
238+
Query: &sdp.Query{
239+
Type: azureshared.NetworkVirtualNetwork.String(),
240+
Method: sdp.QueryMethod_GET,
241+
Query: vnetName,
242+
Scope: linkedScope,
243+
},
244+
})
245+
}
246+
}
247+
248+
return sdpItem, nil
249+
}
250+
251+
func (n networkNatGatewayWrapper) GetLookups() sources.ItemTypeLookups {
252+
return sources.ItemTypeLookups{
253+
NetworkNatGatewayLookupByName,
254+
}
255+
}
256+
257+
func (n networkNatGatewayWrapper) PotentialLinks() map[shared.ItemType]bool {
258+
return map[shared.ItemType]bool{
259+
azureshared.NetworkPublicIPAddress: true,
260+
azureshared.NetworkPublicIPPrefix: true,
261+
azureshared.NetworkSubnet: true,
262+
azureshared.NetworkVirtualNetwork: true,
263+
}
264+
}
265+
266+
func (n networkNatGatewayWrapper) TerraformMappings() []*sdp.TerraformMapping {
267+
return []*sdp.TerraformMapping{
268+
{
269+
TerraformMethod: sdp.QueryMethod_GET,
270+
TerraformQueryMap: "azurerm_nat_gateway.name",
271+
},
272+
}
273+
}
274+
275+
func (n networkNatGatewayWrapper) IAMPermissions() []string {
276+
return []string{
277+
"Microsoft.Network/natGateways/read",
278+
}
279+
}
280+
281+
func (n networkNatGatewayWrapper) PredefinedRole() string {
282+
return "Reader"
283+
}

0 commit comments

Comments
 (0)