2026-01-16 15:55:43 -08:00
|
|
|
// Copyright IBM Corp. 2021, 2025
|
2023-01-03 22:03:54 +00:00
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// Package plugin implements the Azure host plugin for Boundary,
|
|
|
|
|
// providing functionality to manage and list Azure hosts.
|
2021-09-07 22:06:22 -04:00
|
|
|
package plugin
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
2025-01-22 18:36:03 +00:00
|
|
|
"sync"
|
2021-09-07 22:06:22 -04:00
|
|
|
"time"
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
|
2021-09-07 22:06:22 -04:00
|
|
|
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
|
|
|
|
|
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
|
2021-11-10 13:59:21 -05:00
|
|
|
"github.com/mitchellh/mapstructure"
|
2025-01-22 18:36:03 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
2021-11-16 16:06:33 -07:00
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
|
"google.golang.org/grpc/status"
|
2021-09-07 22:06:22 -04:00
|
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
|
|
|
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
2021-09-07 22:06:22 -04:00
|
|
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AzurePlugin implements the HostPluginServiceServer interface
|
|
|
|
|
type AzurePlugin struct {
|
|
|
|
|
pb.UnimplementedHostPluginServiceServer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure that we are implementing HostPluginServiceServer
|
2025-01-22 18:36:03 +00:00
|
|
|
var _ pb.HostPluginServiceServer = (*AzurePlugin)(nil)
|
2021-09-07 22:06:22 -04:00
|
|
|
|
2021-11-23 08:57:08 -07:00
|
|
|
type SetAttributes struct {
|
|
|
|
|
Filter string `mapstructure:"filter"`
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
type azureClients struct {
|
|
|
|
|
resClient *resources.Client
|
|
|
|
|
vmClient *compute.VirtualMachinesClient
|
|
|
|
|
vmssClient *compute.VirtualMachineScaleSetsClient
|
|
|
|
|
vmssvmClient *compute.VirtualMachineScaleSetVMsClient
|
|
|
|
|
ifClient *network.InterfacesClient
|
|
|
|
|
pipClient *network.PublicIPAddressesClient
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-23 08:57:08 -07:00
|
|
|
func (p *AzurePlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
2021-11-16 16:06:33 -07:00
|
|
|
if err := validateCatalog(req.GetCatalog()); err != nil {
|
|
|
|
|
return nil, err
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
if err := validateSecrets(req.GetCatalog().GetSecrets()); err != nil {
|
|
|
|
|
return nil, err
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-16 16:06:33 -07:00
|
|
|
secrets := req.GetCatalog().GetSecrets().AsMap()
|
|
|
|
|
persistedSecrets := map[string]interface{}{
|
|
|
|
|
constSecretValue: secrets[constSecretValue],
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
if _, ok := secrets[constSecretId]; ok {
|
|
|
|
|
persistedSecrets[constSecretId] = secrets[constSecretId]
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
persist, err := structpb.NewStruct(persistedSecrets)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
2021-11-16 16:06:33 -07:00
|
|
|
return nil, status.Errorf(codes.FailedPrecondition, "failed marshaling persisted secrets: %q", err.Error())
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
|
|
|
|
|
return &pb.OnCreateCatalogResponse{
|
|
|
|
|
Persisted: &pb.HostCatalogPersisted{
|
|
|
|
|
Secrets: persist,
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-23 08:57:08 -07:00
|
|
|
func (p *AzurePlugin) OnUpdateCatalog(_ context.Context, req *pb.OnUpdateCatalogRequest) (*pb.OnUpdateCatalogResponse, error) {
|
2021-11-16 16:06:33 -07:00
|
|
|
if err := validateCatalog(req.GetNewCatalog()); err != nil {
|
|
|
|
|
return nil, err
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
currentCatalog := req.GetCurrentCatalog()
|
|
|
|
|
if currentCatalog == nil {
|
2021-11-16 16:06:33 -07:00
|
|
|
return nil, status.Error(codes.FailedPrecondition, "current catalog is nil")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
secrets := req.GetNewCatalog().GetSecrets()
|
2021-09-07 22:06:22 -04:00
|
|
|
if secrets == nil {
|
2021-11-10 15:26:19 -07:00
|
|
|
return &pb.OnUpdateCatalogResponse{}, nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
if err := validateSecrets(secrets); err != nil {
|
|
|
|
|
return nil, err
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-16 16:06:33 -07:00
|
|
|
secretsMap := secrets.AsMap()
|
|
|
|
|
persistedSecrets := map[string]interface{}{
|
|
|
|
|
constSecretValue: secretsMap[constSecretValue],
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
if _, ok := secretsMap[constSecretId]; ok {
|
|
|
|
|
persistedSecrets[constSecretId] = secretsMap[constSecretId]
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
persist, err := structpb.NewStruct(persistedSecrets)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
2021-11-16 16:06:33 -07:00
|
|
|
return nil, status.Errorf(codes.FailedPrecondition, "failed marshaling persisted secrets: %q", err.Error())
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-16 16:06:33 -07:00
|
|
|
return &pb.OnUpdateCatalogResponse{
|
|
|
|
|
Persisted: &pb.HostCatalogPersisted{
|
|
|
|
|
Secrets: persist,
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-23 08:57:08 -07:00
|
|
|
func (p *AzurePlugin) OnCreateSet(_ context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
|
|
|
|
|
if err := validateSet(req.GetSet()); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &pb.OnCreateSetResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *AzurePlugin) OnUpdateSet(_ context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
|
|
|
|
if err := validateSet(req.GetNewSet()); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &pb.OnUpdateSetResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-07 22:06:22 -04:00
|
|
|
func (p *AzurePlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
|
|
|
|
|
if len(req.GetSets()) == 0 {
|
2021-11-10 15:26:19 -07:00
|
|
|
return &pb.ListHostsResponse{}, nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
|
2021-09-07 22:06:22 -04:00
|
|
|
if catalog == nil {
|
2021-11-16 16:06:33 -07:00
|
|
|
return nil, status.Error(codes.FailedPrecondition, "catalog is nil")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
catalogAttrs := catalog.GetAttributes()
|
|
|
|
|
if catalogAttrs == nil {
|
2021-11-16 16:06:33 -07:00
|
|
|
return nil, status.Error(codes.FailedPrecondition, "catalog has no attributes")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// Create authorizer
|
|
|
|
|
catalog.Secrets = secrets
|
2021-11-10 13:59:21 -05:00
|
|
|
authzInfo, err := getAuthorizationInfo(catalog)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
2021-11-10 13:59:21 -05:00
|
|
|
return nil, fmt.Errorf("error fetching authorizationInfo: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if authzInfo == nil {
|
2021-11-16 16:06:33 -07:00
|
|
|
return nil, status.Error(codes.FailedPrecondition, "authorization info is nil")
|
2021-11-10 13:59:21 -05:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
authorizer, err := authzInfo.autorestAuthorizer()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error fetching autorest authorizer: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
if authorizer == nil {
|
|
|
|
|
return nil, errors.New("fetched authorizer is nil")
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// Set up common client options
|
|
|
|
|
commonOpts := []Option{
|
2021-11-10 13:59:21 -05:00
|
|
|
WithSubscriptionId(authzInfo.AuthParams.SubscriptionId),
|
2021-09-07 22:06:22 -04:00
|
|
|
WithAuthorizer(authorizer),
|
2025-01-22 18:36:03 +00:00
|
|
|
}
|
2021-09-07 22:06:22 -04:00
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// Add base URL if specified
|
2021-09-07 22:06:22 -04:00
|
|
|
catalogAttrFields := catalogAttrs.GetFields()
|
|
|
|
|
if catalogAttrFields[constBaseUrl] != nil {
|
|
|
|
|
if baseUrl := catalogAttrFields[constBaseUrl].GetStringValue(); baseUrl != "" {
|
|
|
|
|
commonOpts = append(commonOpts, WithBaseUrl(baseUrl))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// 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)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
if clients.vmClient, initErr = getVirtualMachinesClient(commonOpts...); initErr != nil {
|
|
|
|
|
return nil, fmt.Errorf("error fetching virtual machines client: %w", initErr)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
if clients.vmssClient, initErr = getVirtualMachineScaleSetsClient(commonOpts...); initErr != nil {
|
|
|
|
|
return nil, fmt.Errorf("error fetching virtual machine scale set client: %w", initErr)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
if clients.vmssvmClient, initErr = getVirtualMachineScaleSetVMsClient(commonOpts...); initErr != nil {
|
|
|
|
|
return nil, fmt.Errorf("error fetching virtual machine scale set vms client: %w", initErr)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
if clients.ifClient, initErr = getNetworkInterfacesClient(commonOpts...); initErr != nil {
|
|
|
|
|
return nil, fmt.Errorf("error fetching network interfaces client: %w", initErr)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
if clients.pipClient, initErr = getPublicIpAddressesClient(commonOpts...); initErr != nil {
|
|
|
|
|
return nil, fmt.Errorf("error fetching public ip address client: %w", initErr)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
return clients, nil
|
|
|
|
|
}
|
2021-09-07 22:06:22 -04:00
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
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
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
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...)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
return vmResources, vmssResources, resourceToSetMap, nil
|
|
|
|
|
}
|
2021-09-07 22:06:22 -04:00
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
func listAndFilterResources(ctx context.Context,
|
|
|
|
|
resClient *resources.Client,
|
|
|
|
|
filter, setID string) (
|
|
|
|
|
vmResources []resources.GenericResourceExpanded,
|
|
|
|
|
vmssResources []resources.GenericResourceExpanded,
|
|
|
|
|
resourceToSetMap map[string][]string,
|
|
|
|
|
_ error) {
|
2021-09-07 22:06:22 -04:00
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2021-09-07 22:06:22 -04:00
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
for iter.NotDone() {
|
|
|
|
|
val := iter.Value()
|
|
|
|
|
if err := iter.NextWithContext(ctx); err != nil {
|
|
|
|
|
return nil, nil, nil, fmt.Errorf("error iterating resources: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
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)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
return vmResources, vmssResources, resourceToSetMap, nil
|
|
|
|
|
}
|
2023-04-17 11:00:31 +01:00
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// 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
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
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
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// ID Parsing Functions
|
2021-09-07 22:06:22 -04:00
|
|
|
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
|
2025-01-22 18:36:03 +00:00
|
|
|
// for some Azure APIs and we have a much more limited scope.
|
2021-09-07 22:06:22 -04:00
|
|
|
splitId := strings.Split(strings.TrimLeft(in, "/"), "/")
|
2025-01-22 18:36:03 +00:00
|
|
|
|
2021-09-07 22:06:22 -04:00
|
|
|
// Run through some sanity checks
|
|
|
|
|
if len(splitId) != 8 ||
|
2022-08-17 14:00:52 -04:00
|
|
|
!strings.EqualFold(splitId[0], constSubscriptions) ||
|
|
|
|
|
!strings.EqualFold(splitId[2], constResourceGroups) ||
|
|
|
|
|
!strings.EqualFold(splitId[4], constProviders) ||
|
|
|
|
|
!strings.EqualFold(splitId[5], expectedService) ||
|
|
|
|
|
!strings.EqualFold(splitId[6], expectedResource) {
|
2021-09-07 22:06:22 -04:00
|
|
|
return "", "", fmt.Errorf("unexpected format of resource ID: %v", splitId)
|
|
|
|
|
}
|
|
|
|
|
return splitId[3], splitId[7], nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
func extractResourceType(resourceID string) (string, error) {
|
|
|
|
|
if resourceID == "" {
|
|
|
|
|
return "", fmt.Errorf("empty resource ID provided")
|
2021-11-16 16:06:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
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)
|
2021-11-16 16:06:33 -07:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
// 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)
|
2021-11-16 16:06:33 -07:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
// Check for Microsoft.Compute in correct position
|
|
|
|
|
if !strings.EqualFold(parts[5], constMsComputeService) {
|
|
|
|
|
return "", nil // Not a compute resource, return empty string without error
|
2021-11-16 16:06:33 -07:00
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
// 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)
|
2021-11-16 16:06:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
return parts[6], nil
|
2021-11-16 16:06:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
// Validation Functions
|
2021-11-23 08:57:08 -07:00
|
|
|
func validateCatalog(c *hostcatalogs.HostCatalog) error {
|
|
|
|
|
if c == nil {
|
2021-11-16 16:06:33 -07:00
|
|
|
return status.Error(codes.InvalidArgument, "catalog is nil")
|
|
|
|
|
}
|
2021-11-23 08:57:08 -07:00
|
|
|
var attrs CatalogAttributes
|
|
|
|
|
attrMap := c.GetAttributes().AsMap()
|
2021-11-16 16:06:33 -07:00
|
|
|
if err := mapstructure.Decode(attrMap, &attrs); err != nil {
|
|
|
|
|
return status.Errorf(codes.InvalidArgument, "error decoding catalog attributes: %s", err)
|
|
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
2021-11-16 16:06:33 -07:00
|
|
|
badFields := make(map[string]string)
|
|
|
|
|
if !attrs.DisableCredentialRotation {
|
|
|
|
|
badFields["attributes.disable_credential_rotation"] = "This field must be set to true."
|
|
|
|
|
}
|
|
|
|
|
if len(attrs.SubscriptionId) == 0 {
|
|
|
|
|
badFields["attributes.subscription_id"] = "This is a required field."
|
|
|
|
|
}
|
|
|
|
|
if len(attrs.ClientId) == 0 {
|
|
|
|
|
badFields["attributes.client_id"] = "This is a required field."
|
|
|
|
|
}
|
|
|
|
|
if len(attrs.TenantId) == 0 {
|
|
|
|
|
badFields["attributes.tenant_id"] = "This is a required field."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for f := range attrMap {
|
2021-11-23 08:57:08 -07:00
|
|
|
if _, ok := allowedCatalogFields[f]; !ok {
|
2021-11-16 16:06:33 -07:00
|
|
|
badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(badFields) > 0 {
|
|
|
|
|
return invalidArgumentError("Invalid arguments in the new catalog", badFields)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-16 16:06:33 -07:00
|
|
|
return nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-23 08:57:08 -07:00
|
|
|
|
2025-01-22 18:36:03 +00:00
|
|
|
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
|
2021-11-23 08:57:08 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateSet(s *hostsets.HostSet) error {
|
|
|
|
|
if s == nil {
|
|
|
|
|
return status.Error(codes.InvalidArgument, "set is nil")
|
|
|
|
|
}
|
|
|
|
|
var attrs SetAttributes
|
|
|
|
|
attrMap := s.GetAttributes().AsMap()
|
|
|
|
|
if err := mapstructure.Decode(attrMap, &attrs); err != nil {
|
|
|
|
|
return status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
|
|
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
2021-11-23 08:57:08 -07:00
|
|
|
badFields := make(map[string]string)
|
|
|
|
|
if _, ok := attrMap[constFilter]; ok && len(attrs.Filter) == 0 {
|
|
|
|
|
badFields["attributes.filter"] = "This field must be not empty."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for f := range attrMap {
|
|
|
|
|
if _, ok := allowedSetFields[f]; !ok {
|
|
|
|
|
badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(badFields) > 0 {
|
|
|
|
|
return invalidArgumentError("Invalid arguments in the new set", badFields)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-01-22 18:36:03 +00:00
|
|
|
|
|
|
|
|
// 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: {},
|
|
|
|
|
}
|