Add ARM Network dependency (#69093)

* Add ARM Network dependency

* Deduplicate scale set names

* make go-mod-tidy-all

* Add ListAll and address feedback

* Limit resource group for scale sets

* Add RG check

* case-insensitive sub id check
This commit is contained in:
Dan Share
2026-08-03 07:31:17 +00:00
committed by GitHub
parent 85cd70ee13
commit 2fa7acb6b3
15 changed files with 1132 additions and 0 deletions
+2
View File
@@ -72,6 +72,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 h1:d
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0/go.mod h1:6z3b+JdBLH0eMzfBex/cvEIoEFVEwXuB0wbgdfN11iM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 h1:3jDMffAwnvs6qmOqhjNVHB29AKxs6brnzJeo65E1YwM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0/go.mod h1:0mKVz3WT8oNjBunT1zD/HPwMleQ72QClMa7Gmsm+6Kc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 h1:DgqO2jYgDEqmN8W5sPP+ZU7Tfxyn+i9RqXtNsX6Enb8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0/go.mod h1:FBChJszHNRdH5AYJ+Y/NgWilJihKa5WcSlFrNnj2eY0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 h1:0hXKrsbh2M6CQyW0TDC9Bsyd99vQmrOxiBTUfQHZjPA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0/go.mod h1:bvZZor36Jg9q9kouuMyfJ+ay77+qK+YUfThXH1FdXjU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
+2
View File
@@ -58,6 +58,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 h1:d
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0/go.mod h1:6z3b+JdBLH0eMzfBex/cvEIoEFVEwXuB0wbgdfN11iM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 h1:3jDMffAwnvs6qmOqhjNVHB29AKxs6brnzJeo65E1YwM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0/go.mod h1:0mKVz3WT8oNjBunT1zD/HPwMleQ72QClMa7Gmsm+6Kc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 h1:DgqO2jYgDEqmN8W5sPP+ZU7Tfxyn+i9RqXtNsX6Enb8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0/go.mod h1:FBChJszHNRdH5AYJ+Y/NgWilJihKa5WcSlFrNnj2eY0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 h1:0hXKrsbh2M6CQyW0TDC9Bsyd99vQmrOxiBTUfQHZjPA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0/go.mod h1:bvZZor36Jg9q9kouuMyfJ+ay77+qK+YUfThXH1FdXjU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
+1
View File
@@ -35,6 +35,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3 v3.3.0
+2
View File
@@ -91,6 +91,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 h1:d
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0/go.mod h1:6z3b+JdBLH0eMzfBex/cvEIoEFVEwXuB0wbgdfN11iM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 h1:3jDMffAwnvs6qmOqhjNVHB29AKxs6brnzJeo65E1YwM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0/go.mod h1:0mKVz3WT8oNjBunT1zD/HPwMleQ72QClMa7Gmsm+6Kc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 h1:DgqO2jYgDEqmN8W5sPP+ZU7Tfxyn+i9RqXtNsX6Enb8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0/go.mod h1:FBChJszHNRdH5AYJ+Y/NgWilJihKa5WcSlFrNnj2eY0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 h1:0hXKrsbh2M6CQyW0TDC9Bsyd99vQmrOxiBTUfQHZjPA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0/go.mod h1:bvZZor36Jg9q9kouuMyfJ+ay77+qK+YUfThXH1FdXjU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
+1
View File
@@ -44,6 +44,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3 v3.3.0 // indirect
+2
View File
@@ -65,6 +65,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 h1:d
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0/go.mod h1:6z3b+JdBLH0eMzfBex/cvEIoEFVEwXuB0wbgdfN11iM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 h1:3jDMffAwnvs6qmOqhjNVHB29AKxs6brnzJeo65E1YwM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0/go.mod h1:0mKVz3WT8oNjBunT1zD/HPwMleQ72QClMa7Gmsm+6Kc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 h1:DgqO2jYgDEqmN8W5sPP+ZU7Tfxyn+i9RqXtNsX6Enb8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0/go.mod h1:FBChJszHNRdH5AYJ+Y/NgWilJihKa5WcSlFrNnj2eY0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 h1:0hXKrsbh2M6CQyW0TDC9Bsyd99vQmrOxiBTUfQHZjPA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0/go.mod h1:bvZZor36Jg9q9kouuMyfJ+ay77+qK+YUfThXH1FdXjU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
+1
View File
@@ -47,6 +47,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3 v3.3.0 // indirect
+2
View File
@@ -91,6 +91,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 h1:d
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0/go.mod h1:6z3b+JdBLH0eMzfBex/cvEIoEFVEwXuB0wbgdfN11iM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 h1:3jDMffAwnvs6qmOqhjNVHB29AKxs6brnzJeo65E1YwM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0/go.mod h1:0mKVz3WT8oNjBunT1zD/HPwMleQ72QClMa7Gmsm+6Kc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 h1:DgqO2jYgDEqmN8W5sPP+ZU7Tfxyn+i9RqXtNsX6Enb8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0/go.mod h1:FBChJszHNRdH5AYJ+Y/NgWilJihKa5WcSlFrNnj2eY0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 h1:0hXKrsbh2M6CQyW0TDC9Bsyd99vQmrOxiBTUfQHZjPA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0/go.mod h1:bvZZor36Jg9q9kouuMyfJ+ay77+qK+YUfThXH1FdXjU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
+1
View File
@@ -56,6 +56,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3 v3.3.0 // indirect
+2
View File
@@ -122,6 +122,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 h1:d
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0/go.mod h1:6z3b+JdBLH0eMzfBex/cvEIoEFVEwXuB0wbgdfN11iM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 h1:3jDMffAwnvs6qmOqhjNVHB29AKxs6brnzJeo65E1YwM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0/go.mod h1:0mKVz3WT8oNjBunT1zD/HPwMleQ72QClMa7Gmsm+6Kc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0 h1:DgqO2jYgDEqmN8W5sPP+ZU7Tfxyn+i9RqXtNsX6Enb8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7 v7.2.0/go.mod h1:FBChJszHNRdH5AYJ+Y/NgWilJihKa5WcSlFrNnj2eY0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 h1:0hXKrsbh2M6CQyW0TDC9Bsyd99vQmrOxiBTUfQHZjPA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0/go.mod h1:bvZZor36Jg9q9kouuMyfJ+ay77+qK+YUfThXH1FdXjU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
+56
View File
@@ -31,6 +31,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3"
@@ -763,3 +764,58 @@ func NewUserAssignedIdentity(subscription, resourceGroupName, resourceName, clie
},
}
}
// ARMNetworkMock mocks armnetwork.InterfacesClient.
type ARMNetworkMock struct {
// NetworkInterfaces maps resource group name to the standalone NICs in it.
NetworkInterfaces map[string][]*armnetwork.Interface
// VMSSNetworkInterfaces maps "<resourceGroup>/<scaleSetName>" to the NICs
// of that uniform scale set.
VMSSNetworkInterfaces map[string][]*armnetwork.Interface
// VMSSListErrs maps scale set names to an error returned when listing
// that scale set's NICs.
VMSSListErrs map[string]error
// NoAuth indicates that the mock should return an access denied error
// for all requests.
NoAuth bool
}
// Fail if ARMNetworkMock does not implement networkInterfacesLister.
var _ networkInterfacesLister = (*ARMNetworkMock)(nil)
func (m *ARMNetworkMock) NewListPager(resourceGroup string, _ *armnetwork.InterfacesClientListOptions) *runtime.Pager[armnetwork.InterfacesClientListResponse] {
return newPagerHelper(m.NoAuth, func() (armnetwork.InterfacesClientListResponse, error) {
return armnetwork.InterfacesClientListResponse{
InterfaceListResult: armnetwork.InterfaceListResult{
Value: m.NetworkInterfaces[resourceGroup],
},
}, nil
})
}
func (m *ARMNetworkMock) NewListAllPager(_ *armnetwork.InterfacesClientListAllOptions) *runtime.Pager[armnetwork.InterfacesClientListAllResponse] {
return newPagerHelper(m.NoAuth, func() (armnetwork.InterfacesClientListAllResponse, error) {
var allNics []*armnetwork.Interface
for _, nics := range m.NetworkInterfaces {
allNics = append(allNics, nics...)
}
return armnetwork.InterfacesClientListAllResponse{
InterfaceListResult: armnetwork.InterfaceListResult{
Value: allNics,
},
}, nil
})
}
func (m *ARMNetworkMock) NewListVirtualMachineScaleSetNetworkInterfacesPager(resourceGroup, scaleSetName string, _ *armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesOptions) *runtime.Pager[armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesResponse] {
return newPagerHelper(m.NoAuth, func() (armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesResponse, error) {
if err := m.VMSSListErrs[scaleSetName]; err != nil {
return armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesResponse{}, err
}
return armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesResponse{
InterfaceListResult: armnetwork.InterfaceListResult{
Value: m.VMSSNetworkInterfaces[resourceGroup+"/"+scaleSetName],
},
}, nil
})
}
+270
View File
@@ -0,0 +1,270 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package azure
import (
"context"
"log/slog"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/utils/slices"
)
// IPConfiguration represents an Azure network interface IP configuration.
type IPConfiguration struct {
// Primary indicates whether this IP configuration is the primary one for the network interface.
Primary bool
// PrivateIP is the private IP address assigned to this IP configuration. This
// field may be in CIDR notation, if this isn't the Primary IP configuration.
PrivateIP string
}
// NetworkInterface represents an Azure network interface.
type NetworkInterface struct {
// ID is the resource ID of the network interface.
ID string
// Name is the resource name of the network interface.
Name string
// Primary indicates whether this network interface is the primary one for the
// virtual machine or virtual machine scale set instance.
Primary bool
// AttachedVMID is the resource ID of the virtual machine the network
// interface is attached to. Empty for network interfaces not attached to any
// virtual machine. Resource IDs are not casing-stable across Azure APIs, so
// compare case-insensitively when matching against VM resource IDs.
AttachedVMID string
// IPConfigurations is a list of IP configurations associated with the network
// interface.
IPConfigurations []IPConfiguration
}
// networkInterfacesLister is satisfied by *armnetwork.InterfacesClient.
type networkInterfacesLister interface {
// NewListPager lists Azure network interfaces in the given resource group.
NewListPager(resourceGroup string, opts *armnetwork.InterfacesClientListOptions) *runtime.Pager[armnetwork.InterfacesClientListResponse]
// NewListAllPager lists all Azure network interfaces in the subscription.
NewListAllPager(opts *armnetwork.InterfacesClientListAllOptions) *runtime.Pager[armnetwork.InterfacesClientListAllResponse]
// NewListVirtualMachineScaleSetNetworkInterfacesPager lists Azure network interfaces in the given resource group and scale set.
NewListVirtualMachineScaleSetNetworkInterfacesPager(resourceGroup, scaleSetName string, opts *armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesOptions) *runtime.Pager[armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesResponse]
}
// NetworkInterfacesClient is an interface for listing Azure network interfaces.
type NetworkInterfacesClient interface {
// ListNetworkInterfaces lists all network interfaces in the given resource group.
ListNetworkInterfaces(ctx context.Context, resourceGroup string, scaleSetIDs ...string) ([]*NetworkInterface, error)
}
type networkInterfacesClient struct {
networkInterfacesLister networkInterfacesLister
logger *slog.Logger
// subscriptionID is the Azure subscription ID to list network interfaces from.
// This is used to validate that any scale set IDs passed are in the same
// subscription.
subscriptionID string
}
func NewNetworkInterfacesClient(subscriptionID string, cred azcore.TokenCredential, options *arm.ClientOptions) (NetworkInterfacesClient, error) {
networkInterfacesClient, err := armnetwork.NewInterfacesClient(subscriptionID, cred, options)
if err != nil {
return nil, trace.Wrap(err, "failed to create Azure network interfaces client")
}
config := NetworkInterfacesClientConfig{
NetworkInterfacesAPI: networkInterfacesClient,
SubscriptionID: subscriptionID,
}
return NewNetworkInterfacesClientByAPI(config), nil
}
// NetworkInterfacesClientConfig is a configuration struct for creating a
// NetworkInterfacesClient.
type NetworkInterfacesClientConfig struct {
NetworkInterfacesAPI networkInterfacesLister
Logger *slog.Logger
// SubscriptionID is the Azure subscription ID to list network interfaces from.
// This is used to validate that any scale set IDs passed are in the same
// subscription.
SubscriptionID string
}
func NewNetworkInterfacesClientByAPI(config NetworkInterfacesClientConfig) NetworkInterfacesClient {
if config.Logger == nil {
config.Logger = slog.Default().With(teleport.ComponentKey, "azure_networkinterfaces_client")
}
return &networkInterfacesClient{
networkInterfacesLister: config.NetworkInterfacesAPI,
logger: config.Logger,
subscriptionID: config.SubscriptionID,
}
}
// ListNetworkInterfaces lists all network interfaces in the given resource group.
//
// scaleSetIDs is a list of resource IDs for the virtual machine scale sets.
// e.g. "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachineScaleSets/<vmss>"
// If a scale set ID is not in the same subscription or resource group, it is
// skipped with a warning.
func (c *networkInterfacesClient) ListNetworkInterfaces(ctx context.Context, resourceGroup string, scaleSetIDs ...string) ([]*NetworkInterface, error) {
standardAndFlexibleNICs, err := c.listStandardAndFlexibleNICs(ctx, resourceGroup)
if err != nil {
return nil, trace.Wrap(err, "failed to list standard and flexible VMSS NICs")
}
// Currently, we're more concerned with listing standard and flexible VMSS
// NICs, so if a uniform VMSS NIC listing fails, it is logged and the process
// continues.
uniformNICs := c.listUniformNICs(ctx, resourceGroup, scaleSetIDs)
return append(standardAndFlexibleNICs, uniformNICs...), nil
}
func (c *networkInterfacesClient) listStandardAndFlexibleNICs(ctx context.Context, resourceGroup string) ([]*NetworkInterface, error) {
var pager apiPager[armnetwork.Interface]
if resourceGroup == types.Wildcard {
pager = newAPIPager(
c.networkInterfacesLister.NewListAllPager(nil),
func(resp armnetwork.InterfacesClientListAllResponse) []*armnetwork.Interface {
return resp.InterfaceListResult.Value
},
)
} else {
pager = newAPIPager(
c.networkInterfacesLister.NewListPager(resourceGroup, nil),
func(resp armnetwork.InterfacesClientListResponse) []*armnetwork.Interface {
return resp.InterfaceListResult.Value
},
)
}
return c.collectNICs(ctx, pager)
}
func (c *networkInterfacesClient) listUniformNICs(ctx context.Context, resourceGroup string, scaleSetIDs []string) []*NetworkInterface {
var allNICs []*NetworkInterface
for _, scaleSetID := range slices.DeduplicateKey(scaleSetIDs, strings.ToLower) {
id, err := arm.ParseResourceID(scaleSetID)
if err != nil {
c.logger.WarnContext(ctx, "Failed to parse scale set ID", "scale_set_id", scaleSetID, "error", err)
continue
}
// Check the resource ID is for a uniform VMSS in the same subscription and resource group.
isComputeNamespace := strings.EqualFold(id.ResourceType.Namespace, "Microsoft.Compute")
isVMSSResourceType := strings.EqualFold(id.ResourceType.Type, "virtualMachineScaleSets")
if !isComputeNamespace || !isVMSSResourceType {
c.logger.WarnContext(ctx, "Skipping non-uniform scale set ID", "scale_set_id", scaleSetID)
continue
}
if !strings.EqualFold(id.SubscriptionID, c.subscriptionID) {
c.logger.WarnContext(ctx, "Skipping scale set ID in a different subscription", "scale_set_id", scaleSetID, "subscription_id", id.SubscriptionID)
continue
}
if resourceGroup != types.Wildcard && !strings.EqualFold(id.ResourceGroupName, resourceGroup) {
c.logger.WarnContext(ctx, "Skipping scale set ID in a different resource group", "scale_set_id", scaleSetID, "resource_group", resourceGroup)
continue
}
pager := newAPIPager(
c.networkInterfacesLister.NewListVirtualMachineScaleSetNetworkInterfacesPager(id.ResourceGroupName, id.Name, nil),
func(resp armnetwork.InterfacesClientListVirtualMachineScaleSetNetworkInterfacesResponse) []*armnetwork.Interface {
return resp.InterfaceListResult.Value
},
)
nics, err := c.collectNICs(ctx, pager)
if err != nil {
c.logger.WarnContext(ctx, "Failed to list NICs from Uniform VMSS", "scale_set_id", scaleSetID, "error", err)
continue
}
allNICs = append(allNICs, nics...)
}
return allNICs
}
func (c *networkInterfacesClient) collectNICs(ctx context.Context, pager apiPager[armnetwork.Interface]) ([]*NetworkInterface, error) {
var nics []*NetworkInterface
for pager.more() {
res, err := pager.nextPage(ctx)
if err != nil {
return nil, trace.Wrap(ConvertResponseError(err))
}
for _, rawNIC := range res {
if rawNIC == nil {
continue
}
nic, err := nicFromArmNetworkInterface(rawNIC)
if err != nil {
c.logger.DebugContext(ctx, "Skipping Azure Network Interface",
"resource_id", StringVal(rawNIC.ID),
"error", err,
)
continue
}
nics = append(nics, nic)
}
}
return nics, nil
}
func nicFromArmNetworkInterface(nic *armnetwork.Interface) (*NetworkInterface, error) {
if nic == nil {
return nil, trace.BadParameter("nil armnetwork.Interface")
}
var primary bool
var attachedVMID string
ipConfigs := []IPConfiguration{}
if nic.Properties != nil {
primary = BoolVal(nic.Properties.Primary)
if nic.Properties.VirtualMachine != nil {
attachedVMID = StringVal(nic.Properties.VirtualMachine.ID)
}
if nic.Properties.IPConfigurations != nil {
for _, ipConfig := range nic.Properties.IPConfigurations {
if ipConfig != nil && ipConfig.Properties != nil {
ipConfigs = append(ipConfigs, IPConfiguration{
Primary: BoolVal(ipConfig.Properties.Primary),
PrivateIP: StringVal(ipConfig.Properties.PrivateIPAddress),
})
}
}
}
}
return &NetworkInterface{
ID: StringVal(nic.ID),
Name: StringVal(nic.Name),
Primary: primary,
AttachedVMID: attachedVMID,
IPConfigurations: ipConfigs,
}, nil
}
// BoolVal converts a pointer of a bool to a bool value.
func BoolVal(b *bool) bool {
if b != nil {
return *b
}
return false
}
+158
View File
@@ -0,0 +1,158 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package azure
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/stretchr/testify/require"
)
// TestNetworkInterfacesClientLiveList exercises the network interfaces client
// against a real Azure subscription, listing the standalone NICs (regular VMs
// and flexible scale set VMs) in a resource group. It is skipped unless
// TELEPORT_TEST_AZURE is set.
//
// Prerequisites:
// - Authenticate so DefaultAzureCredential can find a credential, e.g. run
// `az login`, or set AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET.
// The identity needs Microsoft.Network/networkInterfaces/read in the
// resource group.
// - Set AZURE_SUBSCRIPTION_ID to the subscription to list from.
// - Set AZURE_RESOURCE_GROUP to a resource group with at least one NIC.
//
// Run with:
//
// TELEPORT_TEST_AZURE=1 \
// AZURE_SUBSCRIPTION_ID=<sub> \
// AZURE_RESOURCE_GROUP=<rg> \
// go test ./lib/cloud/azure/ -run TestNetworkInterfacesClientLiveList -v
func TestNetworkInterfacesClientLiveList(t *testing.T) {
nicClient, resourceGroup := newLiveNetworkInterfacesClient(t)
ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second)
defer cancel()
nics, err := nicClient.ListNetworkInterfaces(ctx, resourceGroup)
require.NoError(t, err)
logNICs(t, nics)
}
// TestNetworkInterfacesClientLiveListUniformVMSS exercises the network
// interfaces client against a real Azure subscription, listing the NICs of
// uniform scale set VMs alongside the standalone NICs in the resource group.
// It is skipped unless TELEPORT_TEST_AZURE, TELEPORT_TEST_AZURE_SUBSCRIPTION_ID
// and TELEPORT_TEST_AZURE_SCALE_SET_IDS are set.
// Prerequisites are the same as TestNetworkInterfacesClientLiveList, plus:
// - Set TELEPORT_TEST_AZURE_SCALE_SET_IDS to a comma-separated list of
// uniform VM scale set IDs in the resource group, e.g. "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1,/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachineScaleSets/vmss2".
//
// Run with:
//
// TELEPORT_TEST_AZURE=1 \
// AZURE_SUBSCRIPTION_ID=<sub> \
// AZURE_RESOURCE_GROUP=<rg> \
// TELEPORT_TEST_AZURE_SCALE_SET_IDS=<vmss1_id,vmss2_id> \
// go test ./lib/cloud/azure/ -run TestNetworkInterfacesClientLiveListUniformVMSS -v
func TestNetworkInterfacesClientLiveListUniformVMSS(t *testing.T) {
nicClient, resourceGroup := newLiveNetworkInterfacesClient(t)
var scaleSetIDs []string
for id := range strings.SplitSeq(os.Getenv("TELEPORT_TEST_AZURE_SCALE_SET_IDS"), ",") {
if id = strings.TrimSpace(id); id != "" {
scaleSetIDs = append(scaleSetIDs, id)
}
}
if len(scaleSetIDs) == 0 {
t.Skip("Set TELEPORT_TEST_AZURE_SCALE_SET_IDS to a comma-separated list of uniform VM scale set IDs.")
}
ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second)
defer cancel()
nics, err := nicClient.ListNetworkInterfaces(ctx, resourceGroup, scaleSetIDs...)
require.NoError(t, err)
// Uniform VMSS NICs are attached to a scale set VM instance, so they can
// be identified by their attached VM's resource ID.
var uniformNICs []*NetworkInterface
for _, nic := range nics {
if strings.Contains(strings.ToLower(nic.AttachedVMID), "/virtualmachinescalesets/") {
uniformNICs = append(uniformNICs, nic)
}
}
require.NotEmpty(t, uniformNICs, "expected the named scale sets to have at least one NIC")
// A uniform VMSS NIC is a child resource of the scale set VM it is
// attached to, so its ID must extend its attached VM's ID.
for _, nic := range uniformNICs {
require.True(t,
strings.HasPrefix(strings.ToLower(nic.ID), strings.ToLower(nic.AttachedVMID)+"/"),
"expected NIC %q to be a child of its attached VM %q", nic.ID, nic.AttachedVMID)
}
logNICs(t, uniformNICs)
}
// newLiveNetworkInterfacesClient builds a NetworkInterfacesClient backed by a
// real Azure credential, skipping the test unless the live-test environment
// variables are set.
func newLiveNetworkInterfacesClient(t *testing.T) (NetworkInterfacesClient, string) {
t.Helper()
if os.Getenv("TELEPORT_TEST_AZURE") == "" {
t.Skip("Set TELEPORT_TEST_AZURE to run this test against a real Azure subscription.")
}
subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID")
if subscriptionID == "" {
t.Skip("Set AZURE_SUBSCRIPTION_ID to the subscription to list network interfaces from.")
}
resourceGroup := os.Getenv("AZURE_RESOURCE_GROUP")
if resourceGroup == "" {
t.Skip("Set AZURE_RESOURCE_GROUP to a resource group with at least one network interface.")
}
cred, err := azidentity.NewDefaultAzureCredential(nil)
require.NoError(t, err, "failed to build a default Azure credential; have you run `az login`?")
nicClient, err := NewNetworkInterfacesClient(subscriptionID, cred, nil)
require.NoError(t, err)
return nicClient, resourceGroup
}
// logNICs logs the listed NICs so live-test runs can be verified by eye, and
// sanity-checks the invariants every real Azure NIC satisfies.
func logNICs(t *testing.T, nics []*NetworkInterface) {
t.Helper()
t.Logf("listed %d NIC(s)", len(nics))
for _, nic := range nics {
require.NotEmpty(t, nic.ID)
require.NotEmpty(t, nic.Name)
t.Logf(" NIC %q: primary=%t attached VM=%q", nic.Name, nic.Primary, nic.AttachedVMID)
for _, ipConfig := range nic.IPConfigurations {
t.Logf(" ipconfig: primary=%t private IP=%q", ipConfig.Primary, ipConfig.PrivateIP)
}
}
}
+253
View File
@@ -0,0 +1,253 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package azure
import (
"context"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
"github.com/gravitational/teleport/lib/utils/log/logtest"
)
// ipConfigGen returns a rapid.Generator that produces arbitrary Azure network
// interface IP configurations.
func ipConfigGen() *rapid.Generator[armnetwork.InterfaceIPConfiguration] {
return rapid.Custom(func(t *rapid.T) armnetwork.InterfaceIPConfiguration {
return armnetwork.InterfaceIPConfiguration{
Name: rapid.Ptr(rapid.String(), true).Draw(t, "ipconfig_name"),
Properties: rapid.Ptr(rapid.Custom(func(t *rapid.T) armnetwork.InterfaceIPConfigurationPropertiesFormat {
return armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: rapid.Ptr(rapid.String(), true).Draw(t, "private_ip"),
Primary: rapid.Ptr(rapid.Bool(), true).Draw(t, "ipconfig_primary"),
}
}), true).Draw(t, "ipconfig_props"),
}
})
}
// nicGen returns a rapid.Generator that produces arbitrary Azure network
// interfaces.
func nicGen() *rapid.Generator[*armnetwork.Interface] {
return rapid.Custom(func(t *rapid.T) *armnetwork.Interface {
return &armnetwork.Interface{
ID: rapid.Ptr(rapid.String(), true).Draw(t, "id"),
Name: rapid.Ptr(rapid.String(), true).Draw(t, "name"),
Properties: rapid.Ptr(rapid.Custom(func(t *rapid.T) armnetwork.InterfacePropertiesFormat {
return armnetwork.InterfacePropertiesFormat{
Primary: rapid.Ptr(rapid.Bool(), true).Draw(t, "primary"),
VirtualMachine: rapid.Ptr(rapid.Custom(func(t *rapid.T) armnetwork.SubResource {
return armnetwork.SubResource{
ID: rapid.Ptr(rapid.String(), true).Draw(t, "vm_id"),
}
}), true).Draw(t, "virtual_machine"),
IPConfigurations: rapid.SliceOfN(rapid.Ptr(ipConfigGen(), true), 0, 5).Draw(t, "ipconfigs"),
}
}), true).Draw(t, "props"),
}
})
}
// TestProperty_nicFromArmNetworkInterface_NeverPanicsOnArbitraryInput ensures that
// nicFromArmNetworkInterface never panics, even when given arbitrary input.
func TestProperty_nicFromArmNetworkInterface_NeverPanicsOnArbitraryInput(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
nicGen := nicGen()
nic, err := nicFromArmNetworkInterface(nicGen.Draw(t, "nic"))
require.NoError(t, err)
if nicGen == nil {
require.Nil(t, nic)
} else {
require.NotNil(t, nic)
}
})
}
// TestProperty_nicFromArmNetworkInterface_FieldPassThrough ensures that all fields
// of an Azure network interface are passed through to the Teleport representation
// of the network interface.
func TestProperty_nicFromArmNetworkInterface_FieldPassThrough(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
rawNIC := nicGen().Draw(t, "nic")
nic, err := nicFromArmNetworkInterface(rawNIC)
require.NoError(t, err)
require.NotNil(t, nic)
if rawNIC.ID != nil {
require.Equal(t, *rawNIC.ID, nic.ID)
} else {
require.Empty(t, nic.ID)
}
if rawNIC.Name != nil {
require.Equal(t, *rawNIC.Name, nic.Name)
} else {
require.Empty(t, nic.Name)
}
wantPrimary := rawNIC.Properties != nil &&
rawNIC.Properties.Primary != nil &&
*rawNIC.Properties.Primary
require.Equal(t, wantPrimary, nic.Primary)
wantVMID := ""
if rawNIC.Properties != nil &&
rawNIC.Properties.VirtualMachine != nil &&
rawNIC.Properties.VirtualMachine.ID != nil {
wantVMID = *rawNIC.Properties.VirtualMachine.ID
}
require.Equal(t, wantVMID, nic.AttachedVMID)
if rawNIC.Properties != nil {
outIdx := 0
for _, ipConfig := range rawNIC.Properties.IPConfigurations {
if ipConfig == nil || ipConfig.Properties == nil {
continue
}
require.Less(t, outIdx, len(nic.IPConfigurations))
got := nic.IPConfigurations[outIdx]
outIdx++
if ipConfig.Properties.Primary != nil {
require.Equal(t, *ipConfig.Properties.Primary, got.Primary)
} else {
require.False(t, got.Primary)
}
if ipConfig.Properties.PrivateIPAddress != nil {
require.Equal(t, *ipConfig.Properties.PrivateIPAddress, got.PrivateIP)
} else {
require.Empty(t, got.PrivateIP)
}
}
require.Len(t, nic.IPConfigurations, outIdx)
}
})
}
// TestProperty_ListNetworkInterfaces_Aggregation ensures that
// ListNetworkInterfaces correctly aggregates network interfaces from multiple
// sources, including standalone network interfaces and those associated with
// virtual machine scale sets.
func TestProperty_ListNetworkInterfaces_Aggregation(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
standalone := rapid.SliceOfN(nicGen(), 0, 5).Draw(t, "standalone")
scaleSets := rapid.MapOfN(
rapid.StringMatching(`vmss-[a-z]{1,5}`),
rapid.SliceOfN(nicGen(), 0, 3),
0, 4,
).Draw(t, "scale_sets")
vmssNICs := make(map[string][]*armnetwork.Interface, len(scaleSets))
vmssErrs := make(map[string]error)
var scaleSetIDs []string
wantCount := len(standalone)
for name, nics := range scaleSets {
scaleSetIDs = append(scaleSetIDs, createVMSSID(rgName, name))
vmssNICs[rgName+"/"+name] = nics
if rapid.Bool().Draw(t, "fails_"+name) {
vmssErrs[name] = trace.AccessDenied("unauthorized")
} else {
wantCount += len(nics)
}
}
client := NewNetworkInterfacesClientByAPI(NetworkInterfacesClientConfig{
NetworkInterfacesAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{rgName: standalone},
VMSSNetworkInterfaces: vmssNICs,
VMSSListErrs: vmssErrs,
},
SubscriptionID: testSubID,
})
got, err := client.ListNetworkInterfaces(t.Context(), rgName, scaleSetIDs...)
require.NoError(t, err)
require.Len(t, got, wantCount)
})
}
// TestProperty_collectNICs_PageBoundaries ensures that collectNICs correctly
// aggregates network interfaces across multiple pages of results.
func TestProperty_collectNICs_PageBoundaries(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
nics := rapid.SliceOfN(
rapid.OneOf(rapid.Just[*armnetwork.Interface](nil), nicGen()),
0, 20,
).Draw(t, "nics")
// Split the NICs into pages at arbitrary boundaries.
var pages [][]*armnetwork.Interface
for remaining := nics; len(remaining) > 0; {
n := rapid.IntRange(1, len(remaining)).Draw(t, "page_size")
pages = append(pages, remaining[:n])
remaining = remaining[n:]
}
if len(pages) == 0 {
// A pager always serves at least one empty page.
pages = [][]*armnetwork.Interface{nil}
}
pageIdx := 0
pager := runtime.NewPager(runtime.PagingHandler[armnetwork.InterfacesClientListResponse]{
More: func(page armnetwork.InterfacesClientListResponse) bool {
return page.NextLink != nil && *page.NextLink != ""
},
Fetcher: func(_ context.Context, _ *armnetwork.InterfacesClientListResponse) (armnetwork.InterfacesClientListResponse, error) {
page := pages[pageIdx]
pageIdx++
var nextLink *string
if pageIdx < len(pages) {
nextLink = to.Ptr("nextpage")
}
return armnetwork.InterfacesClientListResponse{
InterfaceListResult: armnetwork.InterfaceListResult{
Value: page,
NextLink: nextLink,
},
}, nil
},
})
c := &networkInterfacesClient{logger: logtest.NewLogger()}
got, err := c.collectNICs(t.Context(), newAPIPager(pager, func(resp armnetwork.InterfacesClientListResponse) []*armnetwork.Interface {
return resp.InterfaceListResult.Value
}))
require.NoError(t, err)
// Check NICs appear converted and in order, no matter how the input was
// split into pages. nicFromArmNetworkInterface is used as the oracle to
// determine the expected output.
var want []*NetworkInterface
for _, rawNIC := range nics {
if rawNIC == nil {
continue
}
nic, err := nicFromArmNetworkInterface(rawNIC)
require.NoError(t, err)
want = append(want, nic)
}
require.Equal(t, want, got)
})
}
+379
View File
@@ -0,0 +1,379 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package azure
import (
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v7"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
)
func TestListNetworkInterfaces(t *testing.T) {
t.Parallel()
const (
scaleSet1 = "vmss1"
scaleSet2 = "vmss2"
)
regularNIC := &armnetwork.Interface{
ID: to.Ptr(createNICID(rgName, "nic1")),
Name: to.Ptr("nic1"),
Properties: &armnetwork.InterfacePropertiesFormat{
Primary: to.Ptr(true),
VirtualMachine: &armnetwork.SubResource{ID: to.Ptr(createVMID(rgName, "vm1"))},
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
{
Name: to.Ptr("ipconfig1"),
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.Ptr("10.0.0.1"),
Primary: to.Ptr(true),
},
},
},
},
}
wantRegularNIC := &NetworkInterface{
ID: createNICID(rgName, "nic1"),
Name: "nic1",
Primary: true,
AttachedVMID: createVMID(rgName, "vm1"),
IPConfigurations: []IPConfiguration{{
Primary: true,
PrivateIP: "10.0.0.1",
}},
}
multiIPConfigNIC := &armnetwork.Interface{
ID: to.Ptr(createNICID(rgName, "nic2")),
Name: to.Ptr("nic2"),
Properties: &armnetwork.InterfacePropertiesFormat{
Primary: to.Ptr(false),
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
{
Name: to.Ptr("ipconfig1"),
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.Ptr("10.0.0.2"),
Primary: to.Ptr(true),
},
},
{
Name: to.Ptr("ipconfig2"),
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.Ptr("10.0.0.128/25"),
Primary: to.Ptr(false),
},
},
{
// IP configuration without properties is skipped.
Name: to.Ptr("ipconfig3"),
},
},
},
}
wantMultiIPConfigNIC := &NetworkInterface{
ID: createNICID(rgName, "nic2"),
Name: "nic2",
IPConfigurations: []IPConfiguration{
{
Primary: true,
PrivateIP: "10.0.0.2",
},
{
Primary: false,
PrivateIP: "10.0.0.128/25",
},
},
}
// A NIC not attached to any VM has no Primary flag and may have no
// properties at all.
detachedNIC := &armnetwork.Interface{
ID: to.Ptr(createNICID(rgName, "nic-detached")),
Name: to.Ptr("nic-detached"),
}
wantDetachedNIC := &NetworkInterface{
ID: createNICID(rgName, "nic-detached"),
Name: "nic-detached",
IPConfigurations: []IPConfiguration{},
}
const otherRG = "rg2"
otherRGNIC := &armnetwork.Interface{
ID: to.Ptr(createNICID(otherRG, "nic-rg2")),
Name: to.Ptr("nic-rg2"),
}
wantOtherRGNIC := &NetworkInterface{
ID: createNICID(otherRG, "nic-rg2"),
Name: "nic-rg2",
IPConfigurations: []IPConfiguration{},
}
scaleSet1NIC := &armnetwork.Interface{
ID: to.Ptr(createVMSSNICID(rgName, scaleSet1, "0", "vmss1-nic")),
Name: to.Ptr("vmss1-nic"),
Properties: &armnetwork.InterfacePropertiesFormat{
Primary: to.Ptr(true),
VirtualMachine: &armnetwork.SubResource{ID: to.Ptr(createVMSSVMID(rgName, scaleSet1, "0"))},
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
{
Name: to.Ptr("ipconfig1"),
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.Ptr("10.0.1.1"),
Primary: to.Ptr(true),
},
},
},
},
}
wantScaleSet1NIC := &NetworkInterface{
ID: createVMSSNICID(rgName, scaleSet1, "0", "vmss1-nic"),
Name: "vmss1-nic",
Primary: true,
AttachedVMID: createVMSSVMID(rgName, scaleSet1, "0"),
IPConfigurations: []IPConfiguration{{
Primary: true,
PrivateIP: "10.0.1.1",
}},
}
scaleSet2NIC := &armnetwork.Interface{
ID: to.Ptr(createVMSSNICID(rgName, scaleSet2, "0", "vmss2-nic")),
Name: to.Ptr("vmss2-nic"),
Properties: &armnetwork.InterfacePropertiesFormat{
Primary: to.Ptr(true),
VirtualMachine: &armnetwork.SubResource{ID: to.Ptr(createVMSSVMID(rgName, scaleSet2, "0"))},
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
{
Name: to.Ptr("ipconfig1"),
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.Ptr("10.0.2.1"),
Primary: to.Ptr(true),
},
},
},
},
}
wantScaleSet2NIC := &NetworkInterface{
ID: createVMSSNICID(rgName, scaleSet2, "0", "vmss2-nic"),
Name: "vmss2-nic",
Primary: true,
AttachedVMID: createVMSSVMID(rgName, scaleSet2, "0"),
IPConfigurations: []IPConfiguration{{
Primary: true,
PrivateIP: "10.0.2.1",
}},
}
for _, tc := range []struct {
desc string
nicAPI *ARMNetworkMock
scaleSetIDs []string
resourceGroup string
want []*NetworkInterface
expectErr require.ErrorAssertionFunc
}{
{
desc: "NICs from regular VMs, no scale set NICs",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {regularNIC},
},
},
resourceGroup: rgName,
want: []*NetworkInterface{wantRegularNIC},
expectErr: require.NoError,
},
{
desc: "resource group without NICs returns nothing",
nicAPI: &ARMNetworkMock{},
resourceGroup: rgName,
expectErr: require.NoError,
},
{
desc: "converts secondary IP configurations and detached NICs",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {multiIPConfigNIC, detachedNIC},
},
},
resourceGroup: rgName,
want: []*NetworkInterface{wantMultiIPConfigNIC, wantDetachedNIC},
expectErr: require.NoError,
},
{
desc: "combines standalone and uniform scale set NICs",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {regularNIC},
},
VMSSNetworkInterfaces: map[string][]*armnetwork.Interface{
rgName + "/" + scaleSet1: {scaleSet1NIC},
rgName + "/" + scaleSet2: {scaleSet2NIC},
},
},
resourceGroup: rgName,
scaleSetIDs: []string{createVMSSID(rgName, scaleSet1), createVMSSID(rgName, scaleSet2)},
want: []*NetworkInterface{wantRegularNIC, wantScaleSet1NIC, wantScaleSet2NIC},
expectErr: require.NoError,
},
{
desc: "scale set NICs are only listed for named scale sets",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {regularNIC},
},
VMSSNetworkInterfaces: map[string][]*armnetwork.Interface{
rgName + "/" + scaleSet1: {scaleSet1NIC},
},
},
resourceGroup: rgName,
want: []*NetworkInterface{wantRegularNIC},
expectErr: require.NoError,
},
{
desc: "failure listing one scale set does not fail the rest",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {regularNIC},
},
VMSSNetworkInterfaces: map[string][]*armnetwork.Interface{
rgName + "/" + scaleSet1: {scaleSet1NIC},
rgName + "/" + scaleSet2: {scaleSet2NIC},
},
VMSSListErrs: map[string]error{
scaleSet1: trace.AccessDenied("unauthorized"),
},
},
resourceGroup: rgName,
scaleSetIDs: []string{createVMSSID(rgName, scaleSet1), createVMSSID(rgName, scaleSet2)},
want: []*NetworkInterface{wantRegularNIC, wantScaleSet2NIC},
expectErr: require.NoError,
},
{
desc: "duplicate scale set IDs are only listed once",
nicAPI: &ARMNetworkMock{
VMSSNetworkInterfaces: map[string][]*armnetwork.Interface{
rgName + "/" + scaleSet1: {scaleSet1NIC},
},
},
resourceGroup: rgName,
scaleSetIDs: []string{
createVMSSID(rgName, scaleSet1),
createVMSSID(rgName, scaleSet1),
strings.ToUpper(createVMSSID(rgName, scaleSet1)),
},
want: []*NetworkInterface{wantScaleSet1NIC},
expectErr: require.NoError,
},
{
desc: "invalid scale set IDs are skipped",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {regularNIC},
},
VMSSNetworkInterfaces: map[string][]*armnetwork.Interface{
rgName + "/" + scaleSet1: {scaleSet1NIC},
},
},
resourceGroup: rgName,
scaleSetIDs: []string{
"not-a-resource-id",
createVMID(rgName, "vm1"),
createVMSSVMID(rgName, scaleSet1, "0"),
strings.ReplaceAll(createVMSSID(rgName, scaleSet1), testSubID, "22222222-2222-2222-2222-222222222222"),
createVMSSID(rgName, scaleSet1),
},
want: []*NetworkInterface{wantRegularNIC, wantScaleSet1NIC},
expectErr: require.NoError,
},
{
desc: "nil NICs in the response are skipped",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {nil, regularNIC},
},
},
resourceGroup: rgName,
want: []*NetworkInterface{wantRegularNIC},
expectErr: require.NoError,
},
{
desc: "auth error fails the whole listing",
nicAPI: &ARMNetworkMock{NoAuth: true},
resourceGroup: rgName,
expectErr: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsAccessDenied(err), "expected AccessDenied error, got %v", err)
},
},
{
desc: "wildcard resource group lists NICs from all resource groups",
nicAPI: &ARMNetworkMock{
NetworkInterfaces: map[string][]*armnetwork.Interface{
rgName: {regularNIC},
otherRG: {otherRGNIC},
},
},
resourceGroup: types.Wildcard,
want: []*NetworkInterface{wantRegularNIC, wantOtherRGNIC},
expectErr: require.NoError,
},
} {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
client := NewNetworkInterfacesClientByAPI(NetworkInterfacesClientConfig{
NetworkInterfacesAPI: tc.nicAPI,
SubscriptionID: testSubID,
})
nics, err := client.ListNetworkInterfaces(
t.Context(),
tc.resourceGroup,
tc.scaleSetIDs...,
)
tc.expectErr(t, err)
require.ElementsMatch(t, tc.want, nics)
})
}
}
func createNICID(resourceGroup, nicName string) string {
return "/subscriptions/" + testSubID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/networkInterfaces/" + nicName
}
func createVMID(resourceGroup, vmName string) string {
return "/subscriptions/" + testSubID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/" + vmName
}
func createVMSSID(resourceGroup, scaleSetName string) string {
return "/subscriptions/" + testSubID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachineScaleSets/" + scaleSetName
}
func createVMSSVMID(resourceGroup, scaleSetName, instanceID string) string {
return createVMSSID(resourceGroup, scaleSetName) + "/virtualMachines/" + instanceID
}
func createVMSSNICID(resourceGroup, scaleSetName, instanceID, nicName string) string {
return createVMSSVMID(resourceGroup, scaleSetName, instanceID) + "/networkInterfaces/" + nicName
}