Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ea98c877 | ||
|
|
84ee22dcfd |
No files matched your search
@@ -16,3 +16,6 @@
|
||||
|
||||
# Test credentials and other items
|
||||
/plugin/private*
|
||||
|
||||
# vscode settings
|
||||
.vscode
|
||||
@@ -4,9 +4,9 @@ This repo contains a Host-type plugin for [HashiCorp
|
||||
Boundary](https://www.boundaryproject.io/) allowing dynamically sourcing hosts
|
||||
from Azure.
|
||||
|
||||
Host sets created with this plugin define filters which select and group like VM
|
||||
resources within Azure; these host sets can in turn be added to targets within
|
||||
Boundary as host sources.
|
||||
Host sets created with this plugin define filters which select and group Virtual Machines
|
||||
and Virtual Machine instances enscapsulated in both Flexible and Uniform Virtual Machine Scale Sets.
|
||||
These host sets can in turn be added to targets within Boundary as host sources.
|
||||
|
||||
At creation or update of a host catalog of this type, configuration of the
|
||||
plugin is performed via the attribute/secret values passed to the create or
|
||||
@@ -15,8 +15,6 @@ are indicated below with the valid fields.
|
||||
|
||||
Current limitations of this plugin:
|
||||
|
||||
- Only VMs can be added to host sets through this plugin, not any other type of
|
||||
compute resource
|
||||
- A client secret must be generated against a registered application with any
|
||||
necessary permissions
|
||||
- For now, credential rotation must be disabled using the `disable_credential_rotation`
|
||||
@@ -76,7 +74,7 @@ The following attributes are valid on an Azure host Set resource:
|
||||
|
||||
## Filters
|
||||
|
||||
Azure Resource Manager is used to find matching VMs before their details are
|
||||
Azure Resource Manager is used to find matching VMs and VM Scale Sets before their details are
|
||||
looked up. There are some limitations with the filtering syntax; specifically,
|
||||
when using tags, other types of filters (such as on resource type) are not
|
||||
allowed. As a result, it will generally be useful to filter directly on tag
|
||||
|
||||
@@ -12,6 +12,7 @@ require (
|
||||
github.com/manicminer/hamilton v0.46.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
google.golang.org/grpc v1.56.3
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
||||
@@ -355,6 +355,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
@@ -65,6 +65,56 @@ func getVirtualMachinesClient(opt ...Option) (*compute.VirtualMachinesClient, er
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func getVirtualMachineScaleSetsClient(opt ...Option) (*compute.VirtualMachineScaleSetsClient, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing ops: %w", err)
|
||||
}
|
||||
if opts.withSubscriptionId == "" {
|
||||
return nil, errors.New("subscription id is empty")
|
||||
}
|
||||
if opts.withAuthorizer == nil {
|
||||
return nil, errors.New("authorizer is nil")
|
||||
}
|
||||
var client compute.VirtualMachineScaleSetsClient
|
||||
switch opts.withBaseUrl {
|
||||
case "":
|
||||
client = compute.NewVirtualMachineScaleSetsClient(opts.withSubscriptionId)
|
||||
default:
|
||||
client = compute.NewVirtualMachineScaleSetsClientWithBaseURI(opts.withBaseUrl, opts.withSubscriptionId)
|
||||
}
|
||||
client.Authorizer = opts.withAuthorizer
|
||||
if err := client.AddToUserAgent(opts.withUserAgent); err != nil {
|
||||
return nil, fmt.Errorf("error adding identifier to user-agent: %w", err)
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func getVirtualMachineScaleSetVMsClient(opt ...Option) (*compute.VirtualMachineScaleSetVMsClient, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing ops: %w", err)
|
||||
}
|
||||
if opts.withSubscriptionId == "" {
|
||||
return nil, errors.New("subscription id is empty")
|
||||
}
|
||||
if opts.withAuthorizer == nil {
|
||||
return nil, errors.New("authorizer is nil")
|
||||
}
|
||||
var client compute.VirtualMachineScaleSetVMsClient
|
||||
switch opts.withBaseUrl {
|
||||
case "":
|
||||
client = compute.NewVirtualMachineScaleSetVMsClient(opts.withSubscriptionId)
|
||||
default:
|
||||
client = compute.NewVirtualMachineScaleSetVMsClientWithBaseURI(opts.withBaseUrl, opts.withSubscriptionId)
|
||||
}
|
||||
client.Authorizer = opts.withAuthorizer
|
||||
if err := client.AddToUserAgent(opts.withUserAgent); err != nil {
|
||||
return nil, fmt.Errorf("error adding identifier to user-agent: %w", err)
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func getNetworkInterfacesClient(opt ...Option) (*network.InterfacesClient, error) {
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
|
||||
+22
-14
@@ -4,22 +4,30 @@
|
||||
package plugin
|
||||
|
||||
const (
|
||||
constBaseUrl = "base_url"
|
||||
// Azure service constants
|
||||
constMsComputeService = "Microsoft.Compute"
|
||||
constMsNetworkService = "Microsoft.Network"
|
||||
constSubscriptions = "subscriptions"
|
||||
constResourceGroups = "resourceGroups"
|
||||
constProviders = "providers"
|
||||
|
||||
// Resource type constants
|
||||
constVirtualMachinesResource = "virtualMachines"
|
||||
constVirtualMachineScaleSetsResource = "virtualMachineScaleSets"
|
||||
constNetworkInterfacesResource = "networkInterfaces"
|
||||
constPublicIpAddressesResource = "publicIPAddresses"
|
||||
|
||||
// Field constants
|
||||
constFilter = "filter"
|
||||
constSubscriptionId = "subscription_id"
|
||||
constClientId = "client_id"
|
||||
constSecretId = "secret_id"
|
||||
constSecretValue = "secret_value"
|
||||
constCredsLastRotatedTime = "creds_last_rotated_time"
|
||||
constSecretId = "secret_id"
|
||||
constTenantId = "tenant_id"
|
||||
constDefaultFilter = "resourceType eq 'Microsoft.Compute/virtualMachines'"
|
||||
constMsComputeService = "Microsoft.Compute"
|
||||
constVirtualMachinesResource = "virtualMachines"
|
||||
constMsNetworkService = "Microsoft.Network"
|
||||
constNetworkInterfacesResource = "networkInterfaces"
|
||||
constPublicIpAddressesResource = "publicIPAddresses"
|
||||
constSubscriptions = "subscriptions"
|
||||
constResourceGroups = "resourceGroups"
|
||||
constProviders = "providers"
|
||||
constDisableCredentialRotation = "disable_credential_rotation"
|
||||
constSubscriptionId = "subscription_id"
|
||||
constTenantId = "tenant_id"
|
||||
constClientId = "client_id"
|
||||
constBaseUrl = "base_url"
|
||||
|
||||
// Worker pool configuration
|
||||
constDefaultWorkers = 1000
|
||||
)
|
||||
@@ -0,0 +1,277 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
|
||||
)
|
||||
|
||||
type networkInfo struct {
|
||||
IpAddresses []string
|
||||
}
|
||||
|
||||
// Common interface for IP configuration processing
|
||||
type networkProcessor interface {
|
||||
processIPConfigurations(ctx context.Context, ipConfigs []network.InterfaceIPConfiguration, ifName string) (*networkInfo, error)
|
||||
getPublicIPAddress(ctx context.Context, publicIPRef *network.PublicIPAddress, ipConf *network.InterfaceIPConfiguration, ifName string) (string, error)
|
||||
getNetworkInterface(ctx context.Context, interfaceID string) (*network.Interface, string, error)
|
||||
}
|
||||
|
||||
// Standard VM network processor
|
||||
type vmNetworkProcessor struct {
|
||||
clients *azureClients
|
||||
resourceGroup string
|
||||
}
|
||||
|
||||
// VMSS network processor
|
||||
type vmssNetworkProcessor struct {
|
||||
clients *azureClients
|
||||
resourceGroup string
|
||||
vmssName string
|
||||
instanceID string
|
||||
}
|
||||
|
||||
func processNetworkProfile(
|
||||
ctx context.Context,
|
||||
interfaces []compute.NetworkInterfaceReference,
|
||||
processor networkProcessor,
|
||||
targetInfo *networkInfo,
|
||||
) error {
|
||||
|
||||
for _, ifaceRef := range interfaces {
|
||||
if ifaceRef.ID == nil {
|
||||
return fmt.Errorf("nil ID for network interface")
|
||||
}
|
||||
|
||||
iface, ifName, err := processor.getNetworkInterface(ctx, *ifaceRef.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if iface.InterfacePropertiesFormat == nil || iface.InterfacePropertiesFormat.IPConfigurations == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
networkInfo, err := processor.processIPConfigurations(ctx, *iface.InterfacePropertiesFormat.IPConfigurations, ifName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetInfo.IpAddresses = append(targetInfo.IpAddresses, networkInfo.IpAddresses...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *vmNetworkProcessor) getNetworkInterface(ctx context.Context, interfaceID string) (*network.Interface, string, error) {
|
||||
ifResGroup, ifName, err := splitId(interfaceID, constMsNetworkService, constNetworkInterfacesResource)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error splitting network interface id %q: %w", interfaceID, err)
|
||||
}
|
||||
|
||||
iface, err := p.clients.ifClient.Get(ctx, ifResGroup, ifName, "")
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error fetching network interface: %w", err)
|
||||
}
|
||||
|
||||
return &iface, ifName, nil
|
||||
}
|
||||
|
||||
func (p *vmNetworkProcessor) processIPConfigurations(
|
||||
ctx context.Context,
|
||||
ipConfigs []network.InterfaceIPConfiguration,
|
||||
ifName string,
|
||||
) (
|
||||
*networkInfo,
|
||||
error,
|
||||
) {
|
||||
tempInfo := &networkInfo{}
|
||||
for _, ipconf := range ipConfigs {
|
||||
// Process private IP
|
||||
if ipconf.PrivateIPAddress != nil {
|
||||
tempInfo.IpAddresses = append(tempInfo.IpAddresses, *ipconf.PrivateIPAddress)
|
||||
}
|
||||
|
||||
// Process public IP if available
|
||||
if ipconf.PublicIPAddress != nil {
|
||||
ipAddress, err := p.getPublicIPAddress(ctx, ipconf.PublicIPAddress, &ipconf, ifName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tempInfo.IpAddresses = append(tempInfo.IpAddresses, ipAddress)
|
||||
}
|
||||
}
|
||||
return tempInfo, nil
|
||||
}
|
||||
|
||||
func (p *vmNetworkProcessor) getPublicIPAddress(
|
||||
ctx context.Context,
|
||||
publicIP *network.PublicIPAddress,
|
||||
ipConf *network.InterfaceIPConfiguration,
|
||||
ifName string,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if publicIP.ID == nil {
|
||||
return "", fmt.Errorf("ip configuration has public IP address info but nil id")
|
||||
}
|
||||
|
||||
ipResGroup, ipName, err := splitId(*publicIP.ID, constMsNetworkService, constPublicIpAddressesResource)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error splitting public ip address id: %w", err)
|
||||
}
|
||||
|
||||
pipInfo, err := p.clients.pipClient.Get(ctx, ipResGroup, ipName, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching public IP information: %w", err)
|
||||
}
|
||||
|
||||
if pipInfo.PublicIPAddressPropertiesFormat == nil {
|
||||
return "", fmt.Errorf("nil public ip address properties format for public ip %q", *publicIP.ID)
|
||||
}
|
||||
|
||||
if pipInfo.PublicIPAddressPropertiesFormat.IPAddress == nil {
|
||||
return "", fmt.Errorf("nil IP address for public ip %q", *publicIP.ID)
|
||||
}
|
||||
|
||||
return *pipInfo.PublicIPAddressPropertiesFormat.IPAddress, nil
|
||||
}
|
||||
|
||||
func (p *vmssNetworkProcessor) getNetworkInterface(ctx context.Context, interfaceID string) (*network.Interface, string, error) {
|
||||
ifName := extractResourceSuffix(interfaceID)
|
||||
|
||||
iface, err := p.clients.ifClient.GetVirtualMachineScaleSetNetworkInterface(
|
||||
ctx,
|
||||
p.resourceGroup,
|
||||
p.vmssName,
|
||||
p.instanceID,
|
||||
ifName,
|
||||
"")
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error fetching VMSS network interface: %w", err)
|
||||
}
|
||||
|
||||
return &iface, ifName, nil
|
||||
}
|
||||
|
||||
func (p *vmssNetworkProcessor) processIPConfigurations(
|
||||
ctx context.Context,
|
||||
ipConfigs []network.InterfaceIPConfiguration,
|
||||
ifName string,
|
||||
) (
|
||||
*networkInfo,
|
||||
error,
|
||||
) {
|
||||
tempInfo := &networkInfo{}
|
||||
for _, ipconf := range ipConfigs {
|
||||
if ipconf.PrivateIPAddress != nil {
|
||||
tempInfo.IpAddresses = append(tempInfo.IpAddresses, *ipconf.PrivateIPAddress)
|
||||
}
|
||||
|
||||
if ipconf.PublicIPAddress != nil {
|
||||
ipAddress, err := p.getPublicIPAddress(ctx, ipconf.PublicIPAddress, &ipconf, ifName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tempInfo.IpAddresses = append(tempInfo.IpAddresses, ipAddress)
|
||||
}
|
||||
}
|
||||
return tempInfo, nil
|
||||
}
|
||||
|
||||
func (p *vmssNetworkProcessor) getPublicIPAddress(
|
||||
ctx context.Context,
|
||||
publicIP *network.PublicIPAddress,
|
||||
ipConf *network.InterfaceIPConfiguration,
|
||||
ifName string,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if publicIP == nil || publicIP.ID == nil || ipConf.Name == nil {
|
||||
return "", fmt.Errorf("invalid public IP configuration")
|
||||
}
|
||||
|
||||
pipName := extractResourceSuffix(*publicIP.ID)
|
||||
pipInfo, err := p.clients.pipClient.GetVirtualMachineScaleSetPublicIPAddress(
|
||||
ctx,
|
||||
p.resourceGroup,
|
||||
p.vmssName,
|
||||
p.instanceID,
|
||||
ifName,
|
||||
*ipConf.Name,
|
||||
pipName,
|
||||
"")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching public IP information: %w", err)
|
||||
}
|
||||
|
||||
if pipInfo.PublicIPAddressPropertiesFormat == nil {
|
||||
return "", fmt.Errorf("nil public ip address properties format")
|
||||
}
|
||||
|
||||
if pipInfo.PublicIPAddressPropertiesFormat.IPAddress == nil {
|
||||
return "", fmt.Errorf("nil IP address for public ip %q", *publicIP.ID)
|
||||
}
|
||||
|
||||
return *pipInfo.PublicIPAddressPropertiesFormat.IPAddress, nil
|
||||
}
|
||||
|
||||
func processVMNetworkInterfaces(
|
||||
ctx context.Context,
|
||||
vm compute.VirtualMachine,
|
||||
clients *azureClients,
|
||||
resourceGroup string,
|
||||
netInfo *networkInfo,
|
||||
) error {
|
||||
|
||||
if vm.VirtualMachineProperties == nil || vm.VirtualMachineProperties.NetworkProfile == nil {
|
||||
return fmt.Errorf("error fetching network profile")
|
||||
}
|
||||
|
||||
processor := &vmNetworkProcessor{
|
||||
clients: clients,
|
||||
resourceGroup: resourceGroup,
|
||||
}
|
||||
|
||||
return processNetworkProfile(ctx, *vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces, processor, netInfo)
|
||||
}
|
||||
|
||||
func processVMSSNetworkInterfaces(
|
||||
ctx context.Context,
|
||||
vmssvm compute.VirtualMachineScaleSetVM,
|
||||
resourceGroup, vmssName string,
|
||||
clients *azureClients,
|
||||
netInfo *networkInfo,
|
||||
) error {
|
||||
|
||||
if vmssvm.VirtualMachineScaleSetVMProperties == nil ||
|
||||
vmssvm.VirtualMachineScaleSetVMProperties.NetworkProfile == nil {
|
||||
return fmt.Errorf("error fetching network profile")
|
||||
}
|
||||
|
||||
if vmssvm.InstanceID == nil {
|
||||
return fmt.Errorf("instance ID is nil")
|
||||
}
|
||||
|
||||
processor := &vmssNetworkProcessor{
|
||||
clients: clients,
|
||||
resourceGroup: resourceGroup,
|
||||
vmssName: vmssName,
|
||||
instanceID: *vmssvm.InstanceID,
|
||||
}
|
||||
|
||||
return processNetworkProfile(ctx,
|
||||
*vmssvm.VirtualMachineScaleSetVMProperties.NetworkProfile.NetworkInterfaces, processor, netInfo)
|
||||
}
|
||||
|
||||
// extractResourceSuffix extracts the resource name from the end of an Azure resource ID
|
||||
func extractResourceSuffix(resourceID string) string {
|
||||
parts := strings.Split(resourceID, "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+366
-289
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package plugin implements the Azure host plugin for Boundary,
|
||||
// providing functionality to manage and list Azure hosts.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
@@ -8,17 +10,20 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
)
|
||||
|
||||
@@ -28,37 +33,19 @@ type AzurePlugin struct {
|
||||
}
|
||||
|
||||
// Ensure that we are implementing HostPluginServiceServer
|
||||
var (
|
||||
_ pb.HostPluginServiceServer = (*AzurePlugin)(nil)
|
||||
)
|
||||
var _ pb.HostPluginServiceServer = (*AzurePlugin)(nil)
|
||||
|
||||
type SetAttributes struct {
|
||||
Filter string `mapstructure:"filter"`
|
||||
}
|
||||
|
||||
func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatalog) (*pb.HostCatalogPersisted, error) {
|
||||
authzInfo, err := getAuthorizationInfo(catalog)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting hauth config when creating catalog: %w", err)
|
||||
}
|
||||
newPass, err := rotateCredential(ctx, authzInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error rotating credentials when creating catalog: %w", err)
|
||||
}
|
||||
if newPass == nil {
|
||||
return nil, errors.New("new credential back from rotate is nil")
|
||||
}
|
||||
newSecrets, err := structpb.NewStruct(map[string]interface{}{
|
||||
constSecretId: *newPass.KeyId,
|
||||
constSecretValue: *newPass.SecretText,
|
||||
constCredsLastRotatedTime: time.Now().Format(time.RFC3339Nano),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error formatting new credential data as struct")
|
||||
}
|
||||
return &pb.HostCatalogPersisted{
|
||||
Secrets: newSecrets,
|
||||
}, err
|
||||
type azureClients struct {
|
||||
resClient *resources.Client
|
||||
vmClient *compute.VirtualMachinesClient
|
||||
vmssClient *compute.VirtualMachineScaleSetsClient
|
||||
vmssvmClient *compute.VirtualMachineScaleSetVMsClient
|
||||
ifClient *network.InterfacesClient
|
||||
pipClient *network.PublicIPAddressesClient
|
||||
}
|
||||
|
||||
func (p *AzurePlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
||||
@@ -94,13 +81,10 @@ func (p *AzurePlugin) OnUpdateCatalog(_ context.Context, req *pb.OnUpdateCatalog
|
||||
}
|
||||
currentCatalog := req.GetCurrentCatalog()
|
||||
if currentCatalog == nil {
|
||||
// Should never happen
|
||||
return nil, status.Error(codes.FailedPrecondition, "current catalog is nil")
|
||||
}
|
||||
secrets := req.GetNewCatalog().GetSecrets()
|
||||
if secrets == nil {
|
||||
// If new secrets weren't passed in, don't rotate what we have on
|
||||
// update.
|
||||
return &pb.OnUpdateCatalogResponse{}, nil
|
||||
}
|
||||
if err := validateSecrets(secrets); err != nil {
|
||||
@@ -142,10 +126,161 @@ func (p *AzurePlugin) OnUpdateSet(_ context.Context, req *pb.OnUpdateSetRequest)
|
||||
|
||||
func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
|
||||
if len(req.GetSets()) == 0 {
|
||||
// Nothing to fetch
|
||||
return &pb.ListHostsResponse{}, nil
|
||||
}
|
||||
catalog := req.GetCatalog()
|
||||
|
||||
// Initialize Azure clients and authorization
|
||||
clients, err := p.initializeAzureResources(req.GetCatalog(), req.GetPersisted().GetSecrets())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find matching resources and map them to sets
|
||||
vmResources, vmssResources, resourceToSetMap, err := p.findMatchingResources(ctx, req.GetSets(), clients.resClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create mutex and errgroup for parallel processing
|
||||
mu := &sync.Mutex{}
|
||||
vmToNetworkMap := make(map[string]networkInfo)
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// Process standard VMs in parallel if they exist
|
||||
if len(vmResources) > 0 {
|
||||
g.Go(func() error {
|
||||
vmNetworkMap, err := p.processStandardVMs(ctx, vmResources, clients)
|
||||
if err != nil {
|
||||
return fmt.Errorf("processing VMs: %w", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for k, v := range vmNetworkMap {
|
||||
vmToNetworkMap[k] = v
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Process VMSS in parallel if they exist
|
||||
if len(vmssResources) > 0 {
|
||||
g.Go(func() error {
|
||||
vmssNetworkMap, err := p.processVMScaleSets(ctx, vmssResources, clients)
|
||||
if err != nil {
|
||||
return fmt.Errorf("processing VMSS: %w", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for k, v := range vmssNetworkMap {
|
||||
vmToNetworkMap[k] = v
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete and check for errors
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := buildHostsResponse(vmToNetworkMap, resourceToSetMap)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatalog) (*pb.HostCatalogPersisted, error) {
|
||||
authzInfo, err := getAuthorizationInfo(catalog)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting auth config when creating catalog: %w", err)
|
||||
}
|
||||
newPass, err := rotateCredential(ctx, authzInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error rotating credentials when creating catalog: %w", err)
|
||||
}
|
||||
if newPass == nil {
|
||||
return nil, errors.New("new credential back from rotate is nil")
|
||||
}
|
||||
newSecrets, err := structpb.NewStruct(map[string]interface{}{
|
||||
constSecretId: *newPass.KeyId,
|
||||
constSecretValue: *newPass.SecretText,
|
||||
constCredsLastRotatedTime: time.Now().Format(time.RFC3339Nano),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error formatting new credential data as struct")
|
||||
}
|
||||
return &pb.HostCatalogPersisted{
|
||||
Secrets: newSecrets,
|
||||
}, err
|
||||
}
|
||||
|
||||
func buildHostsResponse(
|
||||
vmToNetworkMap map[string]networkInfo,
|
||||
resourceToSetMap map[string][]string) *pb.ListHostsResponse {
|
||||
|
||||
ret := &pb.ListHostsResponse{}
|
||||
|
||||
for resourceID, networkInfo := range vmToNetworkMap {
|
||||
host, err := createHostFromResource(resourceID, networkInfo, resourceToSetMap)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating host from resource: %v\n", err)
|
||||
continue
|
||||
}
|
||||
ret.Hosts = append(ret.Hosts, host)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func createHostFromResource(resourceID string, networkInfo networkInfo,
|
||||
resourceToSetMap map[string][]string) (*pb.ListHostsResponseHost, error) {
|
||||
|
||||
// Extract the resource type from the ID
|
||||
resourceType, err := extractResourceType(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid resource ID: %w", err)
|
||||
}
|
||||
|
||||
switch resourceType {
|
||||
case constVirtualMachineScaleSetsResource:
|
||||
externalName, err := getExternalNameforVMSSInstance(resourceID)
|
||||
if err != nil {
|
||||
externalName = "" // Use empty name if there's an error
|
||||
}
|
||||
|
||||
setId, err := getSetForVMSSInstance(resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine set for resource ID: %w", err)
|
||||
}
|
||||
return &pb.ListHostsResponseHost{
|
||||
ExternalId: resourceID,
|
||||
ExternalName: externalName,
|
||||
IpAddresses: networkInfo.IpAddresses,
|
||||
SetIds: resourceToSetMap[setId],
|
||||
}, nil
|
||||
|
||||
case constVirtualMachinesResource:
|
||||
_, externalName, err := splitId(resourceID, constMsComputeService, constVirtualMachinesResource)
|
||||
if err != nil {
|
||||
externalName = "" // Use empty name if there's an error
|
||||
}
|
||||
|
||||
return &pb.ListHostsResponseHost{
|
||||
ExternalId: resourceID,
|
||||
ExternalName: externalName,
|
||||
IpAddresses: networkInfo.IpAddresses,
|
||||
SetIds: resourceToSetMap[resourceID],
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", resourceType) // Return explicit error
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AzurePlugin) initializeAzureResources(
|
||||
catalog *hostcatalogs.HostCatalog,
|
||||
secrets *structpb.Struct) (*azureClients, error) {
|
||||
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "catalog is nil")
|
||||
}
|
||||
@@ -154,8 +289,8 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
|
||||
return nil, status.Error(codes.FailedPrecondition, "catalog has no attributes")
|
||||
}
|
||||
|
||||
// Create an authorizer
|
||||
catalog.Secrets = req.GetPersisted().GetSecrets()
|
||||
// Create authorizer
|
||||
catalog.Secrets = secrets
|
||||
authzInfo, err := getAuthorizationInfo(catalog)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching authorizationInfo: %w", err)
|
||||
@@ -163,6 +298,7 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
|
||||
if authzInfo == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "authorization info is nil")
|
||||
}
|
||||
|
||||
authorizer, err := authzInfo.autorestAuthorizer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching autorest authorizer: %w", err)
|
||||
@@ -171,16 +307,13 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
|
||||
return nil, errors.New("fetched authorizer is nil")
|
||||
}
|
||||
|
||||
commonOpts := make([]Option, 0, 3)
|
||||
commonOpts = append(commonOpts,
|
||||
// Set up common client options
|
||||
commonOpts := []Option{
|
||||
WithSubscriptionId(authzInfo.AuthParams.SubscriptionId),
|
||||
WithAuthorizer(authorizer),
|
||||
)
|
||||
}
|
||||
|
||||
// Getting authorizer will fail if the attr fields are not populated, so
|
||||
// it's safe to access directly at this point.
|
||||
//
|
||||
// TODO: plumb base URL everywhere, not just here
|
||||
// Add base URL if specified
|
||||
catalogAttrFields := catalogAttrs.GetFields()
|
||||
if catalogAttrFields[constBaseUrl] != nil {
|
||||
if baseUrl := catalogAttrFields[constBaseUrl].GetStringValue(); baseUrl != "" {
|
||||
@@ -188,229 +321,137 @@ func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (
|
||||
}
|
||||
}
|
||||
|
||||
// Create clients
|
||||
resClient, err := getResourcesClient(commonOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching resources client: %w", err)
|
||||
}
|
||||
if resClient == nil {
|
||||
return nil, errors.New("resource client is nil")
|
||||
}
|
||||
vmClient, err := getVirtualMachinesClient(commonOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching virtual machines client: %w", err)
|
||||
}
|
||||
if vmClient == nil {
|
||||
return nil, errors.New("virtual machines client is nil")
|
||||
}
|
||||
ifClient, err := getNetworkInterfacesClient(commonOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching network interfaces client: %w", err)
|
||||
}
|
||||
if ifClient == nil {
|
||||
return nil, errors.New("network interfaces client is nil")
|
||||
}
|
||||
pipClient, err := getPublicIpAddressesClient(commonOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching public ip address client: %w", err)
|
||||
}
|
||||
if pipClient == nil {
|
||||
return nil, errors.New("public ip address client is nil")
|
||||
// Initialize clients
|
||||
clients := &azureClients{}
|
||||
var initErr error
|
||||
|
||||
// Initialize individual clients with error handling
|
||||
if clients.resClient, initErr = getResourcesClient(commonOpts...); initErr != nil {
|
||||
return nil, fmt.Errorf("error fetching resources client: %w", initErr)
|
||||
}
|
||||
|
||||
// This next section finds resource IDs that match to the filter in each
|
||||
// host set, and stores a mapping of, for each resource ID, which host sets
|
||||
// it belongs to
|
||||
resourceToSetMap := make(map[string][]string, len(req.GetSets())*10)
|
||||
var resourceInfos []resources.GenericResourceExpanded
|
||||
for _, set := range req.GetSets() {
|
||||
if set == nil {
|
||||
return nil, errors.New("set is nil")
|
||||
}
|
||||
setAttrs := set.GetAttributes()
|
||||
if setAttrs == nil {
|
||||
return nil, fmt.Errorf("set %s has no attributes", set.GetId())
|
||||
}
|
||||
setAttrFields := setAttrs.GetFields()
|
||||
if len(setAttrFields) == 0 {
|
||||
return nil, fmt.Errorf("set %s attributes has no filter", set.GetId())
|
||||
}
|
||||
|
||||
filter := constDefaultFilter
|
||||
if setAttrFields[constFilter] != nil {
|
||||
if filter = setAttrFields[constFilter].GetStringValue(); filter == "" {
|
||||
return nil, fmt.Errorf("set %s filter is empty", set.GetId())
|
||||
}
|
||||
}
|
||||
|
||||
// List values matching the filter; ask for provisioning state information
|
||||
// to ensure we are seeing only fully provisioned machines
|
||||
{
|
||||
iter, err := resClient.ListComplete(ctx, filter, "", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing resources: %w", err)
|
||||
}
|
||||
for iter.NotDone() {
|
||||
val := iter.Value()
|
||||
if err := iter.NextWithContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("error iterating resources: %w", err)
|
||||
}
|
||||
if val.ID == nil { // something went wrong, likely iterator has advanced beyond the end
|
||||
continue
|
||||
}
|
||||
if val.Type == nil || !strings.EqualFold(*val.Type, "Microsoft.Compute/virtualMachines") {
|
||||
// no point continuing if we can't validate that it's a VM
|
||||
continue
|
||||
}
|
||||
resourceInfos = append(resourceInfos, val)
|
||||
resourceToSetMap[*val.ID] = append(resourceToSetMap[*val.ID], set.GetId())
|
||||
}
|
||||
}
|
||||
if clients.vmClient, initErr = getVirtualMachinesClient(commonOpts...); initErr != nil {
|
||||
return nil, fmt.Errorf("error fetching virtual machines client: %w", initErr)
|
||||
}
|
||||
|
||||
// At this point we have a list of distinct resources and a mapping between
|
||||
// them and the set(s) they belong to. Iterate through them and fetch
|
||||
// information.
|
||||
//
|
||||
// Listing returns IDs which are basically URLs that have key-value
|
||||
// pairs but not the individual components. Get requires the individual
|
||||
// components, and won't accept the ID...
|
||||
//
|
||||
// Anyways, now we find VM details for those resources
|
||||
vmToIfaceMap := make(map[string][]network.Interface)
|
||||
{
|
||||
for _, res := range resourceInfos {
|
||||
resourceGroup, name, err := splitId(*res.ID, constMsComputeService, constVirtualMachinesResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error splitting vm id %q: %w", *res.ID, err)
|
||||
}
|
||||
vm, err := vmClient.Get(ctx, resourceGroup, name, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching vm with id %q: %w", *res.ID, err)
|
||||
}
|
||||
|
||||
iv, err := vmClient.InstanceView(ctx, resourceGroup, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching instance view for vm with id %q: %w", *res.ID, err)
|
||||
}
|
||||
if iv.Statuses == nil {
|
||||
return nil, fmt.Errorf("instance view statuses returned for vm with id %q is null", *res.ID)
|
||||
}
|
||||
var running bool
|
||||
for _, s := range *iv.Statuses {
|
||||
if s.Code == nil {
|
||||
continue
|
||||
}
|
||||
state := strings.ToLower(*s.Code)
|
||||
prefix := "powerstate/"
|
||||
if !strings.HasPrefix(state, prefix) {
|
||||
continue
|
||||
}
|
||||
state = strings.TrimPrefix(state, prefix)
|
||||
if state == "running" {
|
||||
running = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !running {
|
||||
continue
|
||||
}
|
||||
|
||||
props := vm.VirtualMachineProperties
|
||||
if props == nil {
|
||||
return nil, fmt.Errorf("error fetching properties for vm with id %q: %w", *res.ID, err)
|
||||
}
|
||||
if props.NetworkProfile == nil {
|
||||
return nil, fmt.Errorf("error fetching network profile for vm with id %q", *res.ID)
|
||||
}
|
||||
|
||||
// Within the VM, catalog the various interfaces
|
||||
var ifaces []network.Interface
|
||||
for _, ifaceRef := range *props.NetworkProfile.NetworkInterfaces {
|
||||
if ifaceRef.ID == nil {
|
||||
return nil, fmt.Errorf("nil ID for network interface for vm with id %q", *res.ID)
|
||||
}
|
||||
ifResGroup, ifName, err := splitId(*ifaceRef.ID, constMsNetworkService, constNetworkInterfacesResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error splitting network interface id %q: %w", *ifaceRef.ID, err)
|
||||
}
|
||||
iface, err := ifClient.Get(ctx, ifResGroup, ifName, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching network interface with id %q: %w", *ifaceRef.ID, err)
|
||||
}
|
||||
if iface.InterfacePropertiesFormat == nil || iface.InterfacePropertiesFormat.IPConfigurations == nil {
|
||||
continue
|
||||
}
|
||||
ifaces = append(ifaces, iface)
|
||||
}
|
||||
vmToIfaceMap[*res.ID] = ifaces
|
||||
}
|
||||
if clients.vmssClient, initErr = getVirtualMachineScaleSetsClient(commonOpts...); initErr != nil {
|
||||
return nil, fmt.Errorf("error fetching virtual machine scale set client: %w", initErr)
|
||||
}
|
||||
|
||||
// Now, fetch IP details for each of the interfaces for each VM
|
||||
type networkInfo struct {
|
||||
IpAddresses []string
|
||||
}
|
||||
vmToNetworkMap := make(map[string]networkInfo)
|
||||
{
|
||||
for vmId, ifaces := range vmToIfaceMap {
|
||||
var netInfo networkInfo
|
||||
for _, iface := range ifaces {
|
||||
for _, ipconf := range *iface.InterfacePropertiesFormat.IPConfigurations {
|
||||
if ipconf.PrivateIPAddress != nil {
|
||||
netInfo.IpAddresses = append(netInfo.IpAddresses, *ipconf.PrivateIPAddress)
|
||||
}
|
||||
if ipconf.PublicIPAddress != nil {
|
||||
if ipconf.PublicIPAddress.ID == nil {
|
||||
return nil, fmt.Errorf("ip configuration %q has public IP address info but nil id", *ipconf.ID)
|
||||
}
|
||||
ipResGroup, ipName, err := splitId(*ipconf.PublicIPAddress.ID, constMsNetworkService, constPublicIpAddressesResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error splitting public ip address id %q: %w", *ipconf.PublicIPAddress.ID, err)
|
||||
}
|
||||
pipInfo, err := pipClient.Get(ctx, ipResGroup, ipName, "")
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching public IP information with resource group %q and name %q: %w", ipResGroup, ipName, err)
|
||||
}
|
||||
}
|
||||
if pipInfo.PublicIPAddressPropertiesFormat == nil {
|
||||
return nil, fmt.Errorf("nil public ip address properties format for public ip %q", *ipconf.PublicIPAddress.ID)
|
||||
}
|
||||
if pipInfo.PublicIPAddressPropertiesFormat.IPAddress != nil {
|
||||
netInfo.IpAddresses = append(netInfo.IpAddresses, *pipInfo.PublicIPAddressPropertiesFormat.IPAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vmToNetworkMap[vmId] = netInfo
|
||||
}
|
||||
if clients.vmssvmClient, initErr = getVirtualMachineScaleSetVMsClient(commonOpts...); initErr != nil {
|
||||
return nil, fmt.Errorf("error fetching virtual machine scale set vms client: %w", initErr)
|
||||
}
|
||||
|
||||
ret := &pb.ListHostsResponse{}
|
||||
for k, v := range vmToNetworkMap {
|
||||
_, externalName, err := splitId(k, constMsComputeService, constVirtualMachinesResource)
|
||||
if err != nil {
|
||||
externalName = ""
|
||||
}
|
||||
|
||||
host := &pb.ListHostsResponseHost{
|
||||
ExternalId: k,
|
||||
ExternalName: externalName,
|
||||
IpAddresses: v.IpAddresses,
|
||||
SetIds: resourceToSetMap[k],
|
||||
}
|
||||
ret.Hosts = append(ret.Hosts, host)
|
||||
if clients.ifClient, initErr = getNetworkInterfacesClient(commonOpts...); initErr != nil {
|
||||
return nil, fmt.Errorf("error fetching network interfaces client: %w", initErr)
|
||||
}
|
||||
return ret, nil
|
||||
|
||||
if clients.pipClient, initErr = getPublicIpAddressesClient(commonOpts...); initErr != nil {
|
||||
return nil, fmt.Errorf("error fetching public ip address client: %w", initErr)
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func (p *AzurePlugin) findMatchingResources(ctx context.Context,
|
||||
sets []*hostsets.HostSet,
|
||||
resClient *resources.Client) (
|
||||
vmResources []resources.GenericResourceExpanded,
|
||||
vmssResources []resources.GenericResourceExpanded,
|
||||
resourceToSetMap map[string][]string,
|
||||
_ error) {
|
||||
|
||||
resourceToSetMap = make(map[string][]string, len(sets)*10) // Pre-allocate map size
|
||||
|
||||
for _, set := range sets {
|
||||
if err := validateHostSet(set); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
filter := getSetFilter(set)
|
||||
setVMResources, setVMSSResources, setResourceToSetMap, err := listAndFilterResources(ctx, resClient, filter, set.GetId())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
vmResources = append(vmResources, setVMResources...)
|
||||
vmssResources = append(vmssResources, setVMSSResources...)
|
||||
for k, v := range setResourceToSetMap {
|
||||
resourceToSetMap[k] = append(resourceToSetMap[k], v...)
|
||||
}
|
||||
}
|
||||
|
||||
return vmResources, vmssResources, resourceToSetMap, nil
|
||||
}
|
||||
|
||||
func listAndFilterResources(ctx context.Context,
|
||||
resClient *resources.Client,
|
||||
filter, setID string) (
|
||||
vmResources []resources.GenericResourceExpanded,
|
||||
vmssResources []resources.GenericResourceExpanded,
|
||||
resourceToSetMap map[string][]string,
|
||||
_ error) {
|
||||
|
||||
resourceToSetMap = make(map[string][]string)
|
||||
iter, err := resClient.ListComplete(ctx, filter, "", nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("error listing resources: %w", err)
|
||||
}
|
||||
|
||||
for iter.NotDone() {
|
||||
val := iter.Value()
|
||||
if err := iter.NextWithContext(ctx); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("error iterating resources: %w", err)
|
||||
}
|
||||
|
||||
if val.ID == nil || val.Type == nil {
|
||||
continue
|
||||
}
|
||||
virtualMachineType := constMsComputeService + "/" + constVirtualMachinesResource
|
||||
virtualMachineScaleSetType := constMsComputeService + "/" + constVirtualMachineScaleSetsResource
|
||||
if strings.EqualFold(*val.Type, virtualMachineType) {
|
||||
vmResources = append(vmResources, val)
|
||||
resourceToSetMap[*val.ID] = append(resourceToSetMap[*val.ID], setID)
|
||||
} else if strings.EqualFold(*val.Type, virtualMachineScaleSetType) {
|
||||
vmssResources = append(vmssResources, val)
|
||||
resourceToSetMap[*val.ID] = append(resourceToSetMap[*val.ID], setID)
|
||||
}
|
||||
}
|
||||
|
||||
return vmResources, vmssResources, resourceToSetMap, nil
|
||||
}
|
||||
|
||||
// Resource Filter Management
|
||||
func getSetFilter(set *hostsets.HostSet) string {
|
||||
setAttrFields := set.GetAttributes().GetFields()
|
||||
if setAttrFields[constFilter] != nil {
|
||||
if filter := setAttrFields[constFilter].GetStringValue(); filter != "" {
|
||||
return filter
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateHostSet(set *hostsets.HostSet) error {
|
||||
if set == nil {
|
||||
return errors.New("set is nil")
|
||||
}
|
||||
setAttrs := set.GetAttributes()
|
||||
if setAttrs == nil {
|
||||
return fmt.Errorf("set %s has no attributes", set.GetId())
|
||||
}
|
||||
if len(setAttrs.GetFields()) == 0 {
|
||||
return fmt.Errorf("set %s attributes has no filter", set.GetId())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ID Parsing Functions
|
||||
func splitId(in, expectedService, expectedResource string) (string, string, error) {
|
||||
// Note: this could be stolen from the helpers directory in the TF AzureRM
|
||||
// provider but I'm not at the moment because it does a bunch of trickery
|
||||
// for some Azure APIs and we have a much more limited scope. I may come to
|
||||
// regret this.
|
||||
// for some Azure APIs and we have a much more limited scope.
|
||||
splitId := strings.Split(strings.TrimLeft(in, "/"), "/")
|
||||
|
||||
// Run through some sanity checks
|
||||
if len(splitId) != 8 ||
|
||||
!strings.EqualFold(splitId[0], constSubscriptions) ||
|
||||
@@ -423,47 +464,39 @@ func splitId(in, expectedService, expectedResource string) (string, string, erro
|
||||
return splitId[3], splitId[7], nil
|
||||
}
|
||||
|
||||
// Returns an invalid argument error with the problematic fields included
|
||||
// in the error message.
|
||||
func invalidArgumentError(msg string, f map[string]string) error {
|
||||
var fieldMsgs []string
|
||||
for field, val := range f {
|
||||
fieldMsgs = append(fieldMsgs, fmt.Sprintf("%q: %q", field, val))
|
||||
func extractResourceType(resourceID string) (string, error) {
|
||||
if resourceID == "" {
|
||||
return "", fmt.Errorf("empty resource ID provided")
|
||||
}
|
||||
if len(fieldMsgs) > 0 {
|
||||
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
|
||||
|
||||
parts := strings.Split(strings.TrimLeft(resourceID, "/"), "/")
|
||||
|
||||
// Validate minimum length and basic structure
|
||||
if len(parts) < 6 {
|
||||
return "", fmt.Errorf("resource ID too short: %s", resourceID)
|
||||
}
|
||||
return status.Error(codes.InvalidArgument, msg)
|
||||
}
|
||||
|
||||
func validateSecrets(s *structpb.Struct) error {
|
||||
if s == nil {
|
||||
return status.Error(codes.InvalidArgument, "Secrets not provided but are required")
|
||||
}
|
||||
var secrets SecretData
|
||||
if err := mapstructure.Decode(s.AsMap(), &secrets); err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "error decoding catalog secrets: %s", err)
|
||||
}
|
||||
badFields := make(map[string]string)
|
||||
if len(secrets.SecretValue) == 0 {
|
||||
badFields["secrets.secret_value"] = "This field is required."
|
||||
}
|
||||
if len(secrets.CredsLastRotatedTime) != 0 {
|
||||
badFields["secrets.creds_last_rotated_time"] = "This field is reserved and cannot be set."
|
||||
}
|
||||
if len(badFields) > 0 {
|
||||
return invalidArgumentError("Error in the secrets provided", badFields)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var allowedCatalogFields = map[string]struct{}{
|
||||
constDisableCredentialRotation: {},
|
||||
constSubscriptionId: {},
|
||||
constTenantId: {},
|
||||
constClientId: {},
|
||||
|
||||
// Check for required segments in correct positions
|
||||
if !strings.EqualFold(parts[0], constSubscriptions) ||
|
||||
!strings.EqualFold(parts[2], constResourceGroups) ||
|
||||
!strings.EqualFold(parts[4], constProviders) {
|
||||
return "", fmt.Errorf("malformed resource ID, missing required segments: %s", resourceID)
|
||||
}
|
||||
|
||||
// Check for Microsoft.Compute in correct position
|
||||
if !strings.EqualFold(parts[5], constMsComputeService) {
|
||||
return "", nil // Not a compute resource, return empty string without error
|
||||
}
|
||||
|
||||
// Ensure there's a resource type after Microsoft.Compute
|
||||
if len(parts) < 7 {
|
||||
return "", fmt.Errorf("no resource type found after Microsoft.Compute: %s", resourceID)
|
||||
}
|
||||
|
||||
return parts[6], nil
|
||||
}
|
||||
|
||||
// Validation Functions
|
||||
func validateCatalog(c *hostcatalogs.HostCatalog) error {
|
||||
if c == nil {
|
||||
return status.Error(codes.InvalidArgument, "catalog is nil")
|
||||
@@ -473,6 +506,7 @@ func validateCatalog(c *hostcatalogs.HostCatalog) error {
|
||||
if err := mapstructure.Decode(attrMap, &attrs); err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "error decoding catalog attributes: %s", err)
|
||||
}
|
||||
|
||||
badFields := make(map[string]string)
|
||||
if !attrs.DisableCredentialRotation {
|
||||
badFields["attributes.disable_credential_rotation"] = "This field must be set to true."
|
||||
@@ -499,8 +533,26 @@ func validateCatalog(c *hostcatalogs.HostCatalog) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var allowedSetFields = map[string]struct{}{
|
||||
constFilter: {},
|
||||
func validateSecrets(s *structpb.Struct) error {
|
||||
if s == nil {
|
||||
return status.Error(codes.InvalidArgument, "Secrets not provided but are required")
|
||||
}
|
||||
var secrets SecretData
|
||||
if err := mapstructure.Decode(s.AsMap(), &secrets); err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "error decoding catalog secrets: %s", err)
|
||||
}
|
||||
|
||||
badFields := make(map[string]string)
|
||||
if len(secrets.SecretValue) == 0 {
|
||||
badFields["secrets.secret_value"] = "This field is required."
|
||||
}
|
||||
if len(secrets.CredsLastRotatedTime) != 0 {
|
||||
badFields["secrets.creds_last_rotated_time"] = "This field is reserved and cannot be set."
|
||||
}
|
||||
if len(badFields) > 0 {
|
||||
return invalidArgumentError("Error in the secrets provided", badFields)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSet(s *hostsets.HostSet) error {
|
||||
@@ -512,6 +564,7 @@ func validateSet(s *hostsets.HostSet) error {
|
||||
if err := mapstructure.Decode(attrMap, &attrs); err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
|
||||
}
|
||||
|
||||
badFields := make(map[string]string)
|
||||
if _, ok := attrMap[constFilter]; ok && len(attrs.Filter) == 0 {
|
||||
badFields["attributes.filter"] = "This field must be not empty."
|
||||
@@ -528,3 +581,27 @@ func validateSet(s *hostsets.HostSet) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Error Handling Functions
|
||||
func invalidArgumentError(msg string, f map[string]string) error {
|
||||
var fieldMsgs []string
|
||||
for field, val := range f {
|
||||
fieldMsgs = append(fieldMsgs, fmt.Sprintf("%q: %q", field, val))
|
||||
}
|
||||
if len(fieldMsgs) > 0 {
|
||||
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
|
||||
}
|
||||
return status.Error(codes.InvalidArgument, msg)
|
||||
}
|
||||
|
||||
// Map Definitions for Allowed Fields
|
||||
var allowedCatalogFields = map[string]struct{}{
|
||||
constDisableCredentialRotation: {},
|
||||
constSubscriptionId: {},
|
||||
constTenantId: {},
|
||||
constClientId: {},
|
||||
}
|
||||
|
||||
var allowedSetFields = map[string]struct{}{
|
||||
constFilter: {},
|
||||
}
|
||||
+288
-2
@@ -7,6 +7,8 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -448,7 +450,7 @@ func testGetHostStructs(t *testing.T) (*hostcatalogs.HostCatalog, []*hostsets.Ho
|
||||
Id: "set1",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
constFilter: constDefaultFilter,
|
||||
constFilter: "",
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -456,7 +458,7 @@ func testGetHostStructs(t *testing.T) (*hostcatalogs.HostCatalog, []*hostsets.Ho
|
||||
Id: "set2",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
constFilter: constDefaultFilter,
|
||||
constFilter: "",
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -505,3 +507,287 @@ func testCreateInitialSecret(t *testing.T, hostCatalog *hostcatalogs.HostCatalog
|
||||
|
||||
return authzInfo, initialCred, cleanup
|
||||
}
|
||||
|
||||
func TestSplitId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedService string
|
||||
expectedResource string
|
||||
wantRG string
|
||||
wantName string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid resource id",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
expectedService: "Microsoft.Compute",
|
||||
expectedResource: "virtualMachines",
|
||||
wantRG: "rg1",
|
||||
wantName: "vm1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid with different case",
|
||||
input: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINES/vm1",
|
||||
expectedService: "Microsoft.Compute",
|
||||
expectedResource: "virtualMachines",
|
||||
wantRG: "rg1",
|
||||
wantName: "vm1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - trailing slash",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1/",
|
||||
expectedService: "Microsoft.Compute",
|
||||
expectedResource: "virtualMachines",
|
||||
wantRG: "",
|
||||
wantName: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - wrong service",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/virtualMachines/vm1",
|
||||
expectedService: "Microsoft.Compute",
|
||||
expectedResource: "virtualMachines",
|
||||
wantRG: "",
|
||||
wantName: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - wrong resource type",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/diskEncryptionSets/des1",
|
||||
expectedService: "Microsoft.Compute",
|
||||
expectedResource: "virtualMachines",
|
||||
wantRG: "",
|
||||
wantName: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - missing segments",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1",
|
||||
expectedService: "Microsoft.Compute",
|
||||
expectedResource: "virtualMachines",
|
||||
wantRG: "",
|
||||
wantName: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - empty string",
|
||||
input: "",
|
||||
expectedService: "Microsoft.Compute",
|
||||
expectedResource: "virtualMachines",
|
||||
wantRG: "",
|
||||
wantName: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotRG, gotName, err := splitId(tt.input, tt.expectedService, tt.expectedResource)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("splitId() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotRG != tt.wantRG {
|
||||
t.Errorf("splitId() gotRG = %v, want %v", gotRG, tt.wantRG)
|
||||
}
|
||||
if gotName != tt.wantName {
|
||||
t.Errorf("splitId() gotName = %v, want %v", gotName, tt.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractResourceType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid compute resource",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
want: "virtualMachines",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid compute resource different case",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/MICROSOFT.COMPUTE/virtualMachines/vm1",
|
||||
want: "virtualMachines",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid with trailing slash",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1/",
|
||||
want: "virtualMachines",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "different compute resource type",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/diskEncryptionSets/des1",
|
||||
want: "diskEncryptionSets",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-compute resource",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/sa1",
|
||||
want: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "malformed - missing provider",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/Microsoft.Compute/virtualMachines/vm1",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "malformed - missing resource type",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
input: "/subscriptions/sub1",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "malformed structure",
|
||||
input: "/something/else/entirely",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := extractResourceType(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("extractResourceType() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("extractResourceType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateHostFromResource(t *testing.T) {
|
||||
// Setup common test data
|
||||
testNetwork := networkInfo{
|
||||
IpAddresses: []string{"192.168.1.1", "192.168.1.2"},
|
||||
}
|
||||
|
||||
// Use full Azure resource paths as keys
|
||||
vmssPath := "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1"
|
||||
vmPath := "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm-resource1"
|
||||
|
||||
resourceToSetMap := map[string][]string{
|
||||
vmssPath: {"set1", "set2"},
|
||||
vmPath: {"set3", "set4"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
resourceID string
|
||||
network networkInfo
|
||||
setMap map[string][]string
|
||||
want *pb.ListHostsResponseHost
|
||||
wantErr bool
|
||||
errMessage string
|
||||
}{
|
||||
{
|
||||
name: "valid VMSS resource",
|
||||
resourceID: vmssPath + "/virtualMachines/vm1",
|
||||
network: testNetwork,
|
||||
setMap: resourceToSetMap,
|
||||
want: &pb.ListHostsResponseHost{
|
||||
ExternalId: vmssPath + "/virtualMachines/vm1",
|
||||
ExternalName: "vmss1_vm1",
|
||||
IpAddresses: []string{"192.168.1.1", "192.168.1.2"},
|
||||
SetIds: []string{"set1", "set2"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid VM resource",
|
||||
resourceID: vmPath,
|
||||
network: testNetwork,
|
||||
setMap: resourceToSetMap,
|
||||
want: &pb.ListHostsResponseHost{
|
||||
ExternalId: vmPath,
|
||||
ExternalName: "vm-resource1",
|
||||
IpAddresses: []string{"192.168.1.1", "192.168.1.2"},
|
||||
SetIds: []string{"set3", "set4"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid resource ID",
|
||||
resourceID: "invalid-resource-id",
|
||||
network: testNetwork,
|
||||
setMap: resourceToSetMap,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
errMessage: "invalid resource ID",
|
||||
},
|
||||
{
|
||||
name: "unsupported resource type",
|
||||
resourceID: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/unsupportedResource/resource1",
|
||||
network: testNetwork,
|
||||
setMap: resourceToSetMap,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
errMessage: "unsupported resource type",
|
||||
},
|
||||
{
|
||||
name: "VMSS with valid resource type but invalid structure",
|
||||
resourceID: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets", // Missing required segments after virtualMachineScaleSets
|
||||
network: testNetwork,
|
||||
setMap: resourceToSetMap,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
errMessage: "could not determine set for resource ID: not a valid VMSS resource ID",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := createHostFromResource(tt.resourceID, tt.network, tt.setMap)
|
||||
|
||||
// Check error cases
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("createHostFromResource() expected error containing %q, got nil", tt.errMessage)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errMessage) {
|
||||
t.Errorf("createHostFromResource() error = %v, want error containing %q", err, tt.errMessage)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check non-error cases
|
||||
if err != nil {
|
||||
t.Errorf("createHostFromResource() unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Compare the results
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("createHostFromResource() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// vmJob represents a single VM processing job
|
||||
type vmJob struct {
|
||||
resource resources.GenericResourceExpanded
|
||||
resourceGroup string
|
||||
name string
|
||||
}
|
||||
|
||||
func (p *AzurePlugin) processStandardVMs(ctx context.Context, resourceInfos []resources.GenericResourceExpanded,
|
||||
clients *azureClients) (map[string]networkInfo, error) {
|
||||
|
||||
// Initialize the map that we'll return
|
||||
vmToNetworkMap := make(map[string]networkInfo)
|
||||
|
||||
// Pre-validate all resource IDs and create jobs slice
|
||||
jobs := make([]vmJob, 0, len(resourceInfos))
|
||||
for _, res := range resourceInfos {
|
||||
resourceGroup, name, err := splitId(*res.ID, constMsComputeService, constVirtualMachinesResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid vm id %q: %w", *res.ID, err)
|
||||
}
|
||||
jobs = append(jobs, vmJob{
|
||||
resource: res,
|
||||
resourceGroup: resourceGroup,
|
||||
name: name,
|
||||
})
|
||||
}
|
||||
|
||||
// Create error group for worker management
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// Create job channel
|
||||
jobsChan := make(chan vmJob)
|
||||
|
||||
// Result handling
|
||||
mu := &sync.Mutex{}
|
||||
|
||||
// Start worker pool
|
||||
for i := 0; i < constDefaultWorkers; i++ {
|
||||
g.Go(func() error {
|
||||
for job := range jobsChan {
|
||||
// Process single VM
|
||||
vm, err := clients.vmClient.Get(ctx, job.resourceGroup, job.name, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching vm with id %q: %w", *job.resource.ID, err)
|
||||
}
|
||||
|
||||
// Only process running VMs
|
||||
running, err := isVMRunning(ctx, clients.vmClient, job.resourceGroup, job.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !running {
|
||||
continue
|
||||
}
|
||||
|
||||
var netInfo networkInfo
|
||||
if err := processVMNetworkInterfaces(ctx, vm, clients, job.resourceGroup, &netInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
vmToNetworkMap[*job.resource.ID] = netInfo
|
||||
mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Send jobs to workers
|
||||
go func() {
|
||||
defer close(jobsChan)
|
||||
for _, job := range jobs {
|
||||
// Check context cancellation
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
jobsChan <- job
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for all workers to complete
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vmToNetworkMap, nil
|
||||
}
|
||||
|
||||
// VM Status Check
|
||||
func isVMRunning(
|
||||
ctx context.Context,
|
||||
vmClient *compute.VirtualMachinesClient,
|
||||
resourceGroup,
|
||||
name string,
|
||||
) (
|
||||
bool,
|
||||
error,
|
||||
) {
|
||||
|
||||
iv, err := vmClient.InstanceView(ctx, resourceGroup, name)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error fetching instance view for vm %s: %w", name, err)
|
||||
}
|
||||
if iv.Statuses == nil {
|
||||
return false, fmt.Errorf("instance view statuses is null for vm %s", name)
|
||||
}
|
||||
|
||||
for _, s := range *iv.Statuses {
|
||||
if s.Code == nil {
|
||||
continue
|
||||
}
|
||||
state := strings.ToLower(*s.Code)
|
||||
if strings.HasPrefix(state, "powerstate/") && strings.TrimPrefix(state, "powerstate/") == "running" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// vmssJob represents a single VMSS processing job
|
||||
type vmssJob struct {
|
||||
resource resources.GenericResourceExpanded
|
||||
resourceGroup string
|
||||
name string
|
||||
}
|
||||
|
||||
// vmssInstanceJob represents a single VMSS instance processing job
|
||||
type vmssInstanceJob struct {
|
||||
vmssvm compute.VirtualMachineScaleSetVM
|
||||
resourceGroup string
|
||||
vmssName string
|
||||
}
|
||||
|
||||
func (p *AzurePlugin) processVMScaleSets(
|
||||
ctx context.Context,
|
||||
resourceInfos []resources.GenericResourceExpanded,
|
||||
clients *azureClients,
|
||||
) (
|
||||
map[string]networkInfo,
|
||||
error,
|
||||
) {
|
||||
|
||||
// Initialize the map that we'll return
|
||||
vmToNetworkMap := make(map[string]networkInfo)
|
||||
|
||||
// Pre-validate all resource IDs and create jobs slice
|
||||
jobs := make([]vmssJob, 0, len(resourceInfos))
|
||||
for _, res := range resourceInfos {
|
||||
resourceGroup, name, err := splitId(*res.ID, constMsComputeService, constVirtualMachineScaleSetsResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid vmss id %q: %w", *res.ID, err)
|
||||
}
|
||||
jobs = append(jobs, vmssJob{
|
||||
resource: res,
|
||||
resourceGroup: resourceGroup,
|
||||
name: name,
|
||||
})
|
||||
}
|
||||
|
||||
// Create error group for worker management
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// Create job channel
|
||||
jobsChan := make(chan vmssJob)
|
||||
|
||||
// Start worker pool
|
||||
for i := 0; i < constDefaultWorkers; i++ {
|
||||
g.Go(func() error {
|
||||
for job := range jobsChan {
|
||||
vmss, err := clients.vmssClient.Get(ctx, job.resourceGroup, job.name, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching vmss with id %q: %w", *job.resource.ID, err)
|
||||
}
|
||||
|
||||
// Check if VMSS properties exist
|
||||
if vmss.VirtualMachineScaleSetProperties == nil {
|
||||
continue
|
||||
}
|
||||
vmssProperties := vmss.VirtualMachineScaleSetProperties
|
||||
|
||||
// Skip if OrchestrationMode is empty or Flexible
|
||||
if vmssProperties.OrchestrationMode == "" {
|
||||
continue
|
||||
}
|
||||
if vmssProperties.OrchestrationMode == compute.OrchestrationModeFlexible {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := processVMSSInstancesParallel(ctx, vmss, job.resourceGroup, clients, vmToNetworkMap); err != nil {
|
||||
return fmt.Errorf("error processing uniform vmss instances: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Send jobs to workers
|
||||
go func() {
|
||||
defer close(jobsChan)
|
||||
for _, job := range jobs {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
jobsChan <- job
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for all workers to complete
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vmToNetworkMap, nil
|
||||
}
|
||||
|
||||
func processVMSSInstancesParallel(
|
||||
ctx context.Context,
|
||||
vmss compute.VirtualMachineScaleSet,
|
||||
resourceGroup string,
|
||||
clients *azureClients,
|
||||
vmToNetworkMap map[string]networkInfo,
|
||||
) error {
|
||||
|
||||
if vmss.Name == nil {
|
||||
return fmt.Errorf("vmss name is nil")
|
||||
}
|
||||
|
||||
vmssvms, err := clients.vmssvmClient.List(ctx, resourceGroup, *vmss.Name, "", "", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching vms for vmss %s: %w", *vmss.Name, err)
|
||||
}
|
||||
|
||||
// Create new error group for instance processing
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
instanceJobs := make(chan vmssInstanceJob)
|
||||
|
||||
// Start instance workers
|
||||
instanceWorkerCount := constDefaultWorkers
|
||||
if len(vmssvms.Values()) < instanceWorkerCount {
|
||||
instanceWorkerCount = len(vmssvms.Values())
|
||||
}
|
||||
|
||||
mu := &sync.Mutex{}
|
||||
|
||||
// Start instance worker pool
|
||||
for i := 0; i < instanceWorkerCount; i++ {
|
||||
g.Go(func() error {
|
||||
for job := range instanceJobs {
|
||||
if job.vmssvm.ID == nil {
|
||||
return fmt.Errorf("nil ID for vmss vm in scale set %s", *vmss.Name)
|
||||
}
|
||||
|
||||
if err := processVMSSInstanceParallel(ctx, job.vmssvm, job.resourceGroup,
|
||||
job.vmssName, clients, vmToNetworkMap, mu); err != nil {
|
||||
return fmt.Errorf("error processing vmss instance %s: %w", *job.vmssvm.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Send instance jobs
|
||||
go func() {
|
||||
defer close(instanceJobs)
|
||||
for _, vmssvm := range vmssvms.Values() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
instanceJobs <- vmssInstanceJob{
|
||||
vmssvm: vmssvm,
|
||||
resourceGroup: resourceGroup,
|
||||
vmssName: *vmss.Name,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func processVMSSInstanceParallel(
|
||||
ctx context.Context,
|
||||
vmssvm compute.VirtualMachineScaleSetVM,
|
||||
resourceGroup,
|
||||
vmssName string,
|
||||
clients *azureClients,
|
||||
vmToNetworkMap map[string]networkInfo,
|
||||
mu *sync.Mutex,
|
||||
) error {
|
||||
|
||||
if vmssvm.InstanceID == nil {
|
||||
return fmt.Errorf("instance ID is nil")
|
||||
}
|
||||
|
||||
// Check if instance is running
|
||||
if running, err := isVMSSInstanceRunning(ctx, clients.vmssvmClient, resourceGroup,
|
||||
vmssName, vmssvm.InstanceID); err != nil {
|
||||
return err
|
||||
} else if !running {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process network interfaces for the instance
|
||||
var netInfo networkInfo
|
||||
if err := processVMSSNetworkInterfaces(ctx, vmssvm, resourceGroup, vmssName, clients, &netInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
vmToNetworkMap[*vmssvm.ID] = netInfo
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VMSS Instance Status Check
|
||||
func isVMSSInstanceRunning(
|
||||
ctx context.Context,
|
||||
vmssvmClient *compute.VirtualMachineScaleSetVMsClient,
|
||||
resourceGroup,
|
||||
vmssName string,
|
||||
instanceID *string,
|
||||
) (
|
||||
bool,
|
||||
error,
|
||||
) {
|
||||
|
||||
if instanceID == nil {
|
||||
return false, fmt.Errorf("instance ID is nil")
|
||||
}
|
||||
|
||||
iv, err := vmssvmClient.GetInstanceView(ctx, resourceGroup, vmssName, *instanceID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error fetching instance view for instance %s: %w", *instanceID, err)
|
||||
}
|
||||
if iv.Statuses == nil {
|
||||
return false, fmt.Errorf("instance view statuses returned for instance %s is null", *instanceID)
|
||||
}
|
||||
|
||||
for _, s := range *iv.Statuses {
|
||||
if s.Code == nil {
|
||||
continue
|
||||
}
|
||||
state := strings.ToLower(*s.Code)
|
||||
prefix := "powerstate/"
|
||||
if !strings.HasPrefix(state, prefix) {
|
||||
continue
|
||||
}
|
||||
state = strings.TrimPrefix(state, prefix)
|
||||
if state == "running" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Helper Functions
|
||||
func getSetForVMSSInstance(in string) (string, error) {
|
||||
if in == "" {
|
||||
return "", fmt.Errorf("empty resource ID provided")
|
||||
}
|
||||
|
||||
// Split the path into segments
|
||||
segments := strings.Split(strings.TrimLeft(in, "/"), "/")
|
||||
|
||||
// Check if this is a VMSS path by looking for required segments
|
||||
if len(segments) < 8 ||
|
||||
!strings.EqualFold(segments[0], constSubscriptions) ||
|
||||
!strings.EqualFold(segments[2], constResourceGroups) ||
|
||||
!strings.EqualFold(segments[4], constProviders) ||
|
||||
!strings.EqualFold(segments[5], constMsComputeService) ||
|
||||
!strings.EqualFold(segments[6], constVirtualMachineScaleSetsResource) {
|
||||
return "", fmt.Errorf("not a valid VMSS resource ID: %s", in)
|
||||
}
|
||||
|
||||
// Reconstruct the VMSS path (everything up to but not including virtualMachines)
|
||||
vmssPath := "/" + strings.Join(segments[:8], "/")
|
||||
|
||||
// Additional validation: if the path contains virtualMachines, it should be in the correct position
|
||||
for i, segment := range segments[8:] {
|
||||
if strings.EqualFold(segment, constVirtualMachinesResource) {
|
||||
if i != 0 { // virtualMachines should be immediately after VMSS name
|
||||
return "", fmt.Errorf("unexpected virtualMachines segment position in resource ID: %s", in)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return vmssPath, nil
|
||||
}
|
||||
|
||||
func getExternalNameforVMSSInstance(in string) (string, error) {
|
||||
splitId := strings.Split(strings.TrimLeft(in, "/"), "/")
|
||||
if len(splitId) != 10 ||
|
||||
!strings.EqualFold(splitId[0], constSubscriptions) ||
|
||||
!strings.EqualFold(splitId[2], constResourceGroups) ||
|
||||
!strings.EqualFold(splitId[4], constProviders) ||
|
||||
!strings.EqualFold(splitId[5], constMsComputeService) ||
|
||||
!strings.EqualFold(splitId[6], constVirtualMachineScaleSetsResource) ||
|
||||
!strings.EqualFold(splitId[8], constVirtualMachinesResource) {
|
||||
return "", fmt.Errorf("unexpected format of virtual machine stateful set ID: %v", splitId)
|
||||
}
|
||||
return splitId[7] + "_" + splitId[9], nil
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetExternalNameforVMSSInstance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid vmss instance id",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1",
|
||||
want: "vmss1_instance1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - vmss instance id with trailing slash",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1/",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "case insensitive segment validation",
|
||||
input: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/vmss1/VIRTUALMACHINES/instance1",
|
||||
want: "vmss1_instance1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - missing segment",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - wrong resource type",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/sa1",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - empty string",
|
||||
input: "",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - malformed id",
|
||||
input: "invalid/format/string",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getExternalNameforVMSSInstance(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getExternalNameforVMSSInstance() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getExternalNameforVMSSInstance() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSetForVMSSInstance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid vmss instance id",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1",
|
||||
want: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid vmss path without instance",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
|
||||
want: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "case insensitive validation",
|
||||
input: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/vmss1",
|
||||
want: "/SUBSCRIPTIONS/sub1/RESOURCEGROUPS/rg1/PROVIDERS/MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/vmss1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "path with trailing slash",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/instance1/",
|
||||
want: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - flexible orchestration mode VM path",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vmss1_instance1",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - wrong provider",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/virtualMachineScaleSets/vmss1",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - malformed path missing segments",
|
||||
input: "/subscriptions/sub1/resourceGroups",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - malformed with virtualMachines in wrong position",
|
||||
input: "/subscriptions/sub1/resourceGroups/rg1/virtualMachines/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - random string",
|
||||
input: "not/a/valid/path",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getSetForVMSSInstance(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getSetForVMSSInstance() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getSetForVMSSInstance() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user