diff --git a/.github/chatmodes/task-researcher.chatmode.md b/.github/chatmodes/task-researcher.chatmode.md index 0f20f710..f7e9302f 100644 --- a/.github/chatmodes/task-researcher.chatmode.md +++ b/.github/chatmodes/task-researcher.chatmode.md @@ -1,6 +1,6 @@ --- description: 'Task research specialist for comprehensive project analysis - Brought to you by microsoft/edge-ai' -tools: ['usages', 'think', 'problems', 'fetch', 'githubRepo', 'runCommands', 'edit/createFile', 'edit/createDirectory', 'edit/editFiles', 'search', 'Bicep (EXPERIMENTAL)/*', 'terraform/*', 'context7/*', 'microsoft-docs/*','runSubagent', 'runSubagent2'] +tools: ['execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalSelection', 'read/terminalLastCommand', 'read/problems', 'read/readFile', 'agent', 'context7/*', 'terraform/*', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'search', 'web', 'azure-mcp/search', 'microsoft-docs/*'] handoffs: - label: "๐ŸŽฏ Start Planning" agent: task-planner diff --git a/blueprints/README.md b/blueprints/README.md index 93a99349..950be9fb 100644 --- a/blueprints/README.md +++ b/blueprints/README.md @@ -65,6 +65,21 @@ Each blueprint in this repository follows a consistent structure: - **CNCF Cluster Script Only**: Ideal for environments with existing infrastructure or custom deployment processes - **Azure Fabric Environment**: For users looking to provision Azure Fabric environments with options to deploy Lakehouse, EventStream, and Fabric workspace +## Testing Blueprints + +Selected blueprints include comprehensive test suites for validation and quality assurance. Tests validate both infrastructure declarations (contract tests) and actual deployments (integration tests). + +**Available Tests:** + +- **Contract Tests** - Fast static validation ensuring output declarations match expectations (zero cost, runs in seconds) +- **Deployment Tests** - Full end-to-end validation creating real Azure resources and testing functionality + +**Blueprints with Test Coverage:** + +- [Full Single Cluster](./full-single-node-cluster/tests/) - Complete test suite for Terraform and Bicep + +See individual blueprint `tests/` directories for detailed testing documentation, setup instructions, and maintainer guidelines. + ## Using Existing Resource Groups All blueprints support deploying to existing resource groups rather than creating new ones. diff --git a/blueprints/full-multi-node-cluster/bicep/README.md b/blueprints/full-multi-node-cluster/bicep/README.md index 13dae4f4..ffc95475 100644 --- a/blueprints/full-multi-node-cluster/bicep/README.md +++ b/blueprints/full-multi-node-cluster/bicep/README.md @@ -7,52 +7,54 @@ Deploys a complete end-to-end environment for Azure IoT Operations on a multi-no ## Parameters -| Name | Description | Type | Default | Required | -|:----------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:---------| -| common | The common component configuration. | `[_5.Common](#user-defined-types)` | n/a | yes | -| resourceGroupName | The name for the resource group. If not provided, a default name will be generated. | `string` | [format('rg-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| useExistingResourceGroup | Whether to use an existing resource group instead of creating a new one. | `bool` | `false` | no | -| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | -| adminPassword | Password used for the host VM. | `securestring` | n/a | yes | -| hostMachineCount | The number of host VMs to create for the cluster. (The first host VM will be the cluster server) | `int` | 3 | no | -| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
Can be retrieved using:

  az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes | -| serverToken | The token that will be given to the server for the cluster or used by agent nodes. (Required for multi-node clusters where hostMachineCount > 1) | `securestring` | n/a | no | -| shouldCreateAcrPrivateEndpoint | Whether to create a private endpoint for the Azure Container Registry. | `bool` | `false` | no | -| shouldDeployAiFoundry | Whether to deploy AI Foundry resources. | `bool` | `false` | no | -| aiFoundryConfig | The AI Foundry configuration settings. | `[_2.AiFoundryConfig](#user-defined-types)` | [variables('_2.aiFoundryConfigDefaults')] | no | -| aiFoundryProjects | Array of AI Foundry projects to create. | `array` | [] | no | -| aiFoundryRaiPolicies | Array of RAI policies to create. | `array` | [] | no | -| aiFoundryModelDeployments | Array of model deployments to create. | `array` | [] | no | -| shouldCreateAiFoundryPrivateEndpoint | Whether to create a private endpoint for AI Foundry. | `bool` | `false` | no | -| shouldEnableNatGateway | Whether to enable NAT Gateway for managed outbound access. | `bool` | `false` | no | -| shouldDisableDefaultOutboundAccess | Whether to disable default outbound access for subnets when NAT gateway is enabled. | `bool` | `true` | no | -| natGatewayPublicIpCount | Number of public IP addresses for NAT Gateway (1-16). | `int` | 1 | no | -| natGatewayIdleTimeoutMinutes | Idle timeout in minutes for NAT gateway connections (4-120). | `int` | 4 | no | -| natGatewayZones | Availability zones for NAT Gateway. Empty array for regional deployment. | `array` | [] | no | -| shouldEnableVpnGateway | Whether to deploy VPN Gateway for remote access. | `bool` | `false` | no | -| vpnGatewayConfig | VPN Gateway configuration settings. | `[_1.VpnGatewayConfig](#user-defined-types)` | [variables('_1.vpnGatewayConfigDefaults')] | no | -| vpnGatewayAzureAdConfig | Azure AD authentication configuration for VPN Gateway. | `[_1.AzureAdConfig](#user-defined-types)` | [variables('_1.azureAdConfigDefaults')] | no | -| shouldEnablePrivateEndpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources. | `bool` | `false` | no | -| shouldEnablePrivateResolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints. | `bool` | `false` | no | -| resolverSubnetAddressPrefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets. | `string` | 10.0.9.0/28 | no | -| shouldEnableKeyVaultPublicNetworkAccess | Whether to enable public network access for the Key Vault. | `bool` | `true` | no | -| shouldEnableStoragePublicNetworkAccess | Whether to enable public network access for the storage account. | `bool` | `true` | no | -| subnetAddressPrefixAcr | Address prefix for the ACR subnet. | `string` | 10.0.4.0/24 | no | -| subnetAddressPrefixAks | Address prefix for the AKS subnet. | `string` | 10.0.5.0/24 | no | -| subnetAddressPrefixAksPod | Address prefix for the AKS pod subnet. | `string` | 10.0.6.0/24 | no | -| shouldCreateAks | Whether to create an Azure Kubernetes Service cluster. | `bool` | `false` | no | -| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | -| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | -| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | -| namespacedDevices | List of namespaced devices to create. | `array` | [] | no | -| assetEndpointProfiles | List of asset endpoint profiles to create. | `array` | [] | no | -| legacyAssets | List of legacy assets to create. | `array` | [] | no | -| namespacedAssets | List of namespaced assets to create. | `array` | [] | no | -| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | -| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no | +| Name | Description | Type | Default | Required | +|:----------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:---------| +| common | The common component configuration. | `[_5.Common](#user-defined-types)` | n/a | yes | +| resourceGroupName | The name for the resource group. If not provided, a default name will be generated. | `string` | [format('rg-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| useExistingResourceGroup | Whether to use an existing resource group instead of creating a new one. | `bool` | `false` | no | +| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | +| adminPassword | Password used for the host VM. | `securestring` | n/a | yes | +| hostMachineCount | The number of host VMs to create for the cluster. (The first host VM will be the cluster server) | `int` | 3 | no | +| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
Can be retrieved using:

  az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes | +| serverToken | The token that will be given to the server for the cluster or used by agent nodes. (Required for multi-node clusters where hostMachineCount > 1) | `securestring` | n/a | no | +| shouldCreateAcrPrivateEndpoint | Whether to create a private endpoint for the Azure Container Registry. | `bool` | `false` | no | +| shouldDeployAiFoundry | Whether to deploy AI Foundry resources. | `bool` | `false` | no | +| aiFoundryConfig | The AI Foundry configuration settings. | `[_2.AiFoundryConfig](#user-defined-types)` | [variables('_2.aiFoundryConfigDefaults')] | no | +| aiFoundryProjects | Array of AI Foundry projects to create. | `[_2.AiProject](#user-defined-types)[]` | [] | no | +| aiFoundryRaiPolicies | Array of RAI policies to create. | `[_2.RaiPolicy](#user-defined-types)[]` | [] | no | +| aiFoundryModelDeployments | Array of model deployments to create. | `[_2.ModelDeployment](#user-defined-types)[]` | [] | no | +| shouldCreateAiFoundryPrivateEndpoint | Whether to create a private endpoint for AI Foundry. | `bool` | `false` | no | +| shouldEnableNatGateway | Whether to enable NAT Gateway for managed outbound access. | `bool` | `false` | no | +| shouldDisableDefaultOutboundAccess | Whether to disable default outbound access for subnets when NAT gateway is enabled. | `bool` | `true` | no | +| natGatewayPublicIpCount | Number of public IP addresses for NAT Gateway (1-16). | `int` | 1 | no | +| natGatewayIdleTimeoutMinutes | Idle timeout in minutes for NAT gateway connections (4-120). | `int` | 4 | no | +| natGatewayZones | Availability zones for NAT Gateway. Empty array for regional deployment. | `string[]` | [] | no | +| shouldEnableVpnGateway | Whether to deploy VPN Gateway for remote access. | `bool` | `false` | no | +| vpnGatewayConfig | VPN Gateway configuration settings. | `[_1.VpnGatewayConfig](#user-defined-types)` | [variables('_1.vpnGatewayConfigDefaults')] | no | +| vpnGatewayAzureAdConfig | Azure AD authentication configuration for VPN Gateway. | `[_1.AzureAdConfig](#user-defined-types)` | [variables('_1.azureAdConfigDefaults')] | no | +| shouldEnablePrivateEndpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources. | `bool` | `false` | no | +| shouldEnablePrivateResolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints. | `bool` | `false` | no | +| resolverSubnetAddressPrefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets. | `string` | 10.0.9.0/28 | no | +| shouldEnableKeyVaultPublicNetworkAccess | Whether to enable public network access for the Key Vault. | `bool` | `true` | no | +| shouldEnableStoragePublicNetworkAccess | Whether to enable public network access for the storage account. | `bool` | `true` | no | +| subnetAddressPrefixAcr | Address prefix for the ACR subnet. | `string` | 10.0.4.0/24 | no | +| subnetAddressPrefixAks | Address prefix for the AKS subnet. | `string` | 10.0.5.0/24 | no | +| subnetAddressPrefixAksPod | Address prefix for the AKS pod subnet. | `string` | 10.0.6.0/24 | no | +| shouldCreateAks | Whether to create an Azure Kubernetes Service cluster. | `bool` | `false` | no | +| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | +| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | +| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | +| namespacedDevices | List of namespaced devices to create. | `[_4.NamespacedDevice](#user-defined-types)[]` | [] | no | +| assetEndpointProfiles | List of asset endpoint profiles to create. | `[_4.AssetEndpointProfile](#user-defined-types)[]` | [] | no | +| legacyAssets | List of legacy assets to create. | `[_4.LegacyAsset](#user-defined-types)[]` | [] | no | +| namespacedAssets | List of namespaced assets to create. | `[_4.NamespacedAsset](#user-defined-types)[]` | [] | no | +| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | +| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `[_3.AkriConnectorTemplate](#user-defined-types)[]` | [] | no | +| registryEndpoints | Custom container registry endpoints to add alongside the default MCR endpoint. | `[_3.RegistryEndpointConfig](#user-defined-types)[]` | [] | no | +| shouldIncludeAcrRegistryEndpoint | Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication. | `bool` | `false` | no | ## Resources @@ -232,6 +234,7 @@ Deploys Azure observability resources including Azure Monitor Workspace, Log Ana | logAnalyticsName | `string` | The Log Analytics Workspace name. | | logAnalyticsId | `string` | The Log Analytics Workspace ID. | | grafanaName | `string` | The Azure Managed Grafana name. | +| grafanaEndpoint | `string` | The Azure Managed Grafana endpoint. | | metricsDataCollectionRuleName | `string` | The metrics data collection rule name. | | logsDataCollectionRuleName | `string` | The logs data collection rule name. | | monitorPrivateLinkScopeId | `string` | Azure Monitor Private Link Scope resource ID. | @@ -709,6 +712,7 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | | shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | | customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no | +| registryEndpoints | List of additional container registry endpoints for pulling custom artifacts. MCR is always added automatically. | `array` | [] | no | | akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no | | customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | | additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `array` | [] | no | @@ -739,27 +743,29 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | postInitScripts | `Microsoft.Resources/deployments` | 2025-04-01 | | iotOpsInstance | `Microsoft.Resources/deployments` | 2025-04-01 | | akriConnectors | `Microsoft.Resources/deployments` | 2025-04-01 | +| registryEndpointsModule | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScriptsSecrets | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScripts | `Microsoft.Resources/deployments` | 2025-04-01 | #### Outputs for edgeIotOps -| Name | Type | Description | -|:---------------------------|:---------|:-------------------------------------------------------------------| -| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | -| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | -| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | -| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | -| customLocationId | `string` | The ID of the deployed Custom Location. | -| customLocationName | `string` | The name of the deployed Custom Location. | -| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | -| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | -| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | -| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | -| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | -| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | +| Name | Type | Description | +|:---------------------------|:---------|:----------------------------------------------------------------------| +| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | +| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | +| aioNamespace | `string` | The namespace in the cluster where Azure IoT Operations is installed. | +| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | +| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | +| customLocationId | `string` | The ID of the deployed Custom Location. | +| customLocationName | `string` | The name of the deployed Custom Location. | +| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | +| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | +| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | +| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | +| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | +| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | ### edgeAssets @@ -1104,6 +1110,14 @@ Trust settings for Akri connector. |:-------------------|:---------|:------------------------------------| | trustListSecretRef | `string` | Reference to the trust list secret. | +### `_3.ArtifactPullSecretSettings` + +Authentication settings for Artifact Pull Secret. + +| Property | Type | Description | +|:----------|:---------|:--------------------------------------------------------------------------| +| secretRef | `string` | The name of the kubernetes secret that contains the artifact pull secret. | + ### `_3.BrokerPersistence` Broker persistence configuration for disk-backed message storage. @@ -1163,6 +1177,57 @@ The mode of the AIO instance feature. Either "Stable", "Preview" or "Disabled". The setting value of the AIO instance feature. Either "Enabled" or "Disabled". +### `_3.RegistryAuthAnonymous` + +Anonymous authentication for registry endpoint. + +| Property | Type | Description | +|:------------------|:---------|:--------------------------------------------------| +| method | `string` | Authentication method. | +| anonymousSettings | `object` | Anonymous authentication settings (empty object). | + +### `_3.RegistryAuthArtifactPullSecret` + +Artifact Pull Secret authentication for registry endpoint. + +| Property | Type | Description | +|:---------------------------|:-------------------------------------------------------|:-------------------------------| +| method | `string` | Authentication method. | +| artifactPullSecretSettings | `[_3.ArtifactPullSecretSettings](#user-defined-types)` | Artifact pull secret settings. | + +### `_3.RegistryAuthentication` + +Authentication configuration for a registry endpoint. + +### `_3.RegistryAuthSystemAssignedManagedIdentity` + +System-Assigned Managed Identity authentication for registry endpoint. + +| Property | Type | Description | +|:--------------------------------------|:------------------------------------------------------------------|:-------------------------------------------| +| method | `string` | Authentication method. | +| systemAssignedManagedIdentitySettings | `[_3.SystemAssignedManagedIdentitySettings](#user-defined-types)` | System-assigned managed identity settings. | + +### `_3.RegistryAuthUserAssignedManagedIdentity` + +User-Assigned Managed Identity authentication for registry endpoint. + +| Property | Type | Description | +|:------------------------------------|:----------------------------------------------------------------|:-----------------------------------------| +| method | `string` | Authentication method. | +| userAssignedManagedIdentitySettings | `[_3.UserAssignedManagedIdentitySettings](#user-defined-types)` | User-assigned managed identity settings. | + +### `_3.RegistryEndpointConfig` + +Container registry endpoint configuration for AIO instance. + +| Property | Type | Description | +|:---------------|:---------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------| +| name | `string` | Unique name for the registry endpoint (3-63 chars, lowercase alphanumeric and hyphens). | +| host | `string` | Container registry hostname (e.g., myregistry.azurecr.io). | +| acrResourceId | `string` | Optional ACR resource ID for automatic AcrPull role assignment. Only applicable when authentication.method is SystemAssignedManagedIdentity. | +| authentication | `[_3.RegistryAuthentication](#user-defined-types)` | Authentication configuration for the registry. | + ### `_3.Release` The common settings for Azure Arc Extensions. @@ -1216,6 +1281,14 @@ The configuration for Self-Signed Issuer for Azure IoT Operations certificates. |:------------|:---------|:------------| | trustSource | `string` | | +### `_3.SystemAssignedManagedIdentitySettings` + +Authentication settings for System-Assigned Managed Identity. + +| Property | Type | Description | +|:---------|:---------|:--------------------------------------------------------------------------------------------------------| +| audience | `string` | Audience of the service to authenticate against. Defaults to "" for ACR. | + ### `_3.TrustConfigSource` The config source of trust for how to use or generate Azure IoT Operations certificates. @@ -1239,6 +1312,16 @@ The configuration for the trust settings of Azure IoT Operations certificates. The source of trust for Azure IoT Operations certificates. +### `_3.UserAssignedManagedIdentitySettings` + +Authentication settings for User-Assigned Managed Identity. + +| Property | Type | Description | +|:---------|:---------|:---------------------------------------------------------------| +| clientId | `string` | Client ID for the user-assigned managed identity. | +| tenantId | `string` | Tenant ID where the managed identity is located. | +| scope | `string` | Resource identifier (application ID URI) with .default suffix. | + ### `_4.AssetAction` Management action configuration for assets. diff --git a/blueprints/full-multi-node-cluster/bicep/main.bicep b/blueprints/full-multi-node-cluster/bicep/main.bicep index e10150b3..f08ee4dd 100644 --- a/blueprints/full-multi-node-cluster/bicep/main.bicep +++ b/blueprints/full-multi-node-cluster/bicep/main.bicep @@ -234,6 +234,32 @@ param shouldEnableAkriSseConnector bool = false @description('List of custom Akri connector templates with user-defined endpoint types and container images.') param customAkriConnectors types.AkriConnectorTemplate[] = [] +@description('Custom container registry endpoints to add alongside the default MCR endpoint.') +param registryEndpoints types.RegistryEndpointConfig[] = [] + +@description('Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication.') +param shouldIncludeAcrRegistryEndpoint bool = false + +/* + Local Variables +*/ + +var acrRegistryEndpoint = shouldIncludeAcrRegistryEndpoint + ? [ + { + name: 'acr-${common.resourcePrefix}' + host: '${cloudAcr.outputs.acrName}.azurecr.io' + acrResourceId: cloudAcr.outputs.acrId + authentication: { + method: 'SystemAssignedManagedIdentity' + systemAssignedManagedIdentitySettings: {} + } + } + ] + : [] + +var combinedRegistryEndpoints = concat(registryEndpoints, acrRegistryEndpoint) + /* Resources */ @@ -494,6 +520,7 @@ module edgeIotOps '../../../src/100-edge/110-iot-ops/bicep/main.bicep' = { shouldEnableAkriOnvifConnector: shouldEnableAkriOnvifConnector shouldEnableAkriSseConnector: shouldEnableAkriSseConnector customAkriConnectors: customAkriConnectors + registryEndpoints: combinedRegistryEndpoints } } diff --git a/blueprints/full-multi-node-cluster/terraform/README.md b/blueprints/full-multi-node-cluster/terraform/README.md index 3cf82632..7dadec3a 100644 --- a/blueprints/full-multi-node-cluster/terraform/README.md +++ b/blueprints/full-multi-node-cluster/terraform/README.md @@ -56,114 +56,116 @@ with the single-node blueprint while preserving multi-node specific capabilities ## Inputs -| Name | Description | Type | Default | Required | -|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|:--------:| -| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes | -| location | Location for all resources in this module | `string` | n/a | yes | -| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes | -| acr\_allow\_trusted\_services | Whether trusted Azure services can bypass ACR network rules | `bool` | `true` | no | -| acr\_allowed\_public\_ip\_ranges | CIDR ranges permitted to reach the ACR public endpoint | `list(string)` | `[]` | no | -| acr\_data\_endpoint\_enabled | Whether to enable the dedicated ACR data endpoint | `bool` | `true` | no | -| acr\_public\_network\_access\_enabled | Whether to enable the ACR public endpoint alongside private connectivity | `bool` | `false` | no | -| acr\_sku | SKU name for the Azure Container Registry | `string` | `"Premium"` | no | -| ai\_foundry\_model\_deployments | Map of model deployments for AI Foundry | ```map(object({ name = string model = object({ format = string name = string version = string }) scale = object({ type = string capacity = number }) rai_policy_name = optional(string) version_upgrade_option = optional(string, "OnceNewDefaultVersionAvailable") }))``` | `{}` | no | -| ai\_foundry\_private\_dns\_zone\_ids | List of private DNS zone IDs for the AI Foundry private endpoint | `list(string)` | `[]` | no | -| ai\_foundry\_projects | Map of AI Foundry projects to create. SKU defaults to 'S0' (currently the only supported value) | ```map(object({ name = string display_name = string description = string sku = optional(string, "S0") }))``` | `{}` | no | -| ai\_foundry\_rai\_policies | Map of Responsible AI (RAI) content filtering policies. Must be created before referenced in model deployments. | ```map(object({ name = string base_policy_name = optional(string, "Microsoft.Default") mode = optional(string, "Blocking") content_filters = optional(list(object({ name = string enabled = optional(bool, true) blocking = optional(bool, true) severity_threshold = optional(string, "Medium") source = string })), []) }))``` | `{}` | no | -| ai\_foundry\_should\_enable\_local\_auth | Whether to enable local (API key) authentication for AI Foundry | `bool` | `true` | no | -| ai\_foundry\_should\_enable\_private\_endpoint | Whether to enable private endpoint for AI Foundry | `bool` | `false` | no | -| ai\_foundry\_should\_enable\_public\_network\_access | Whether to enable public network access to AI Foundry | `bool` | `true` | no | -| ai\_foundry\_sku | SKU name for the AI Foundry account | `string` | `"S0"` | no | -| aio\_features | AIO instance features with mode ('Stable', 'Preview', 'Disabled') and settings ('Enabled', 'Disabled') | ```map(object({ mode = optional(string) settings = optional(map(string)) }))``` | `null` | no | -| aks\_private\_dns\_zone\_id | ID of the private DNS zone to use for AKS private cluster. Use 'system', 'none', or a resource ID | `string` | `null` | no | -| aks\_should\_enable\_private\_cluster | Whether to enable private cluster mode for AKS | `bool` | `false` | no | -| aks\_should\_enable\_private\_cluster\_public\_fqdn | Whether to create a private cluster public FQDN for AKS | `bool` | `false` | no | -| arc\_machine\_count | Number of Arc-enabled machines to target for the cluster when should\_use\_arc\_machines is true | `number` | `1` | no | -| arc\_machine\_name\_prefix | Prefix for the Arc-enabled machine names; otherwise resource\_prefix when should\_use\_arc\_machines is true | `string` | `null` | no | -| arc\_machine\_resource\_group\_name | Resource group name that contains the Arc-enabled servers when should\_use\_arc\_machines is true | `string` | `null` | no | -| azureml\_ml\_workload\_subjects | Custom Kubernetes service account subjects for AzureML workload federation. Example: ['system:serviceaccount:azureml:azureml-workload', 'system:serviceaccount:osmo:osmo-workload'] | `list(string)` | `null` | no | -| azureml\_registry\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning registry when deployed | `bool` | `true` | no | -| azureml\_should\_create\_compute\_cluster | Whether to create a compute cluster for Azure Machine Learning training workloads | `bool` | `true` | no | -| azureml\_should\_create\_ml\_workload\_identity | Whether to create a user-assigned managed identity for AzureML workload federation. | `bool` | `false` | no | -| azureml\_should\_deploy\_registry | Whether to deploy Azure Machine Learning registry resources alongside the workspace | `bool` | `false` | no | -| azureml\_should\_enable\_private\_endpoint | Whether to enable a private endpoint for the Azure Machine Learning workspace | `bool` | `false` | no | -| azureml\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning workspace | `bool` | `true` | no | -| certificate\_subject | Certificate subject information for auto-generated certificates | ```object({ common_name = optional(string, "Full Multi Node VPN Gateway Root Certificate") organization = optional(string, "Edge AI Accelerator") organizational_unit = optional(string, "IT") country = optional(string, "US") province = optional(string, "WA") locality = optional(string, "Redmond") })``` | `{}` | no | -| certificate\_validity\_days | Validity period in days for auto-generated certificates | `number` | `365` | no | -| cluster\_server\_host\_machine\_username | Username for the Arc or VM host machines that receive kube-config during setup Otherwise, resource\_prefix when the user exists on the machine | `string` | `null` | no | -| cluster\_server\_ip | IP address for the cluster server used by node machines when should\_use\_arc\_machines is true | `string` | `null` | no | -| custom\_akri\_connectors | List of custom Akri connector templates with user-defined endpoint types and container images. Supports built-in types (rest, media, onvif, sse) or custom types with custom\_endpoint\_type and custom\_image\_name. Built-in connectors default to mcr.microsoft.com/azureiotoperations/akri-connectors/connector\_type:0.5.1. | ```list(object({ name = string type = string // "rest", "media", "onvif", "sse", "custom" // Custom Connector Fields (required when type = "custom") custom_endpoint_type = optional(string) // e.g., "Contoso.Modbus", "Acme.CustomProtocol" custom_image_name = optional(string) // e.g., "my_acr.azurecr.io/custom-connector" custom_endpoint_version = optional(string, "1.0") // Runtime Configuration (defaults applied based on connector type) registry = optional(string) // Defaults: mcr.microsoft.com for built-in types image_tag = optional(string) // Defaults: 0.5.1 for built-in types, latest for custom replicas = optional(number, 1) image_pull_policy = optional(string) // Default: IfNotPresent // Diagnostics log_level = optional(string) // Default: info (lowercase: trace, debug, info, warning, error, critical) // MQTT Override (uses shared config if not provided) mqtt_config = optional(object({ host = string audience = string ca_configmap = string keep_alive_seconds = optional(number, 60) max_inflight_messages = optional(number, 100) session_expiry_seconds = optional(number, 600) })) // Optional Advanced Fields aio_min_version = optional(string) aio_max_version = optional(string) allocation = optional(object({ policy = string // "Bucketized" bucket_size = number // 1-100 })) additional_configuration = optional(map(string)) secrets = optional(list(object({ secret_alias = string secret_key = string secret_ref = string }))) trust_settings = optional(object({ trust_list_secret_ref = string })) }))``` | `[]` | no | -| custom\_locations\_oid | The object id of the Custom Locations Entra ID application for your tenant If none is provided, the script attempts to retrieve this value which requires 'Application.Read.All' or 'Directory.Read.All' permissions ```sh az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv``` | `string` | `null` | no | -| dns\_prefix | DNS prefix for the AKS cluster. When null a value is generated | `string` | `null` | no | -| enable\_auto\_scaling | Whether to enable auto-scaling for the default node pool | `bool` | `false` | no | -| existing\_certificate\_name | Name of the existing certificate in Key Vault when vpn\_gateway\_should\_generate\_ca is false | `string` | `null` | no | -| host\_machine\_count | Number of edge host virtual machines to create for the multi-node cluster | `number` | `3` | no | -| instance | Instance identifier for naming resources: 001, 002, etc | `string` | `"001"` | no | -| max\_count | Maximum node count for the default node pool | `number` | `null` | no | -| min\_count | Minimum node count for the default node pool | `number` | `null` | no | -| namespaced\_assets | List of namespaced assets with enhanced configuration support | ```list(object({ name = string display_name = optional(string) device_ref = optional(object({ device_name = string endpoint_name = string })) asset_endpoint_profile_ref = optional(string) default_datasets_configuration = optional(string) default_streams_configuration = optional(string) default_events_configuration = optional(string) description = optional(string) documentation_uri = optional(string) enabled = optional(bool, true) hardware_revision = optional(string) manufacturer = optional(string) manufacturer_uri = optional(string) model = optional(string) product_code = optional(string) serial_number = optional(string) software_revision = optional(string) attributes = optional(map(string), {}) datasets = optional(list(object({ name = string data_points = list(object({ data_point_configuration = optional(string) data_source = string name = string observability_mode = optional(string) rest_sampling_interval_ms = optional(number) rest_mqtt_topic = optional(string) rest_include_state_store = optional(bool) rest_state_store_key = optional(string) })) dataset_configuration = optional(string) data_source = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) type_ref = optional(string) })), []) streams = optional(list(object({ name = string stream_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })), []) event_groups = optional(list(object({ name = string data_source = optional(string) event_group_configuration = optional(string) type_ref = optional(string) default_destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) events = list(object({ name = string data_source = string event_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })) })), []) management_groups = optional(list(object({ name = string data_source = optional(string) management_group_configuration = optional(string) type_ref = optional(string) default_topic = optional(string) default_timeout_in_seconds = optional(number, 100) actions = list(object({ name = string action_type = string target_uri = string topic = optional(string) timeout_in_seconds = optional(number) action_configuration = optional(string) type_ref = optional(string) })) })), []) }))``` | `[]` | no | -| namespaced\_devices | List of namespaced devices to create; otherwise, an empty list | ```list(object({ name = string enabled = optional(bool, true) endpoints = object({ outbound = optional(object({ assigned = object({}) }), { assigned = {} }) inbound = map(object({ endpoint_type = string address = string version = optional(string, null) additionalConfiguration = optional(string) authentication = object({ method = string usernamePasswordCredentials = optional(object({ usernameSecretName = string passwordSecretName = string })) x509Credentials = optional(object({ certificateSecretName = string })) }) trustSettings = optional(object({ trustList = string })) })) }) }))``` | `[]` | no | -| nat\_gateway\_idle\_timeout\_minutes | Idle timeout in minutes for NAT gateway connections | `number` | `4` | no | -| nat\_gateway\_public\_ip\_count | Number of public IP addresses to associate with the NAT gateway (example: 2) | `number` | `1` | no | -| nat\_gateway\_zones | Availability zones for NAT gateway resources when zone redundancy is required (example: ['1','2']) | `list(string)` | `[]` | no | -| node\_count | Number of nodes for the agent pool in the AKS cluster | `number` | `1` | no | -| node\_pools | Additional node pools for the AKS cluster; map key is used as the node pool name | ```map(object({ node_count = number vm_size = string subnet_address_prefixes = list(string) pod_subnet_address_prefixes = list(string) node_taints = optional(list(string), []) enable_auto_scaling = optional(bool, false) min_count = optional(number, null) max_count = optional(number, null) }))``` | `{}` | no | -| node\_vm\_size | VM size for the agent pool in the AKS cluster | `string` | `"Standard_D8ds_v5"` | no | -| postgresql\_admin\_password | Administrator password for PostgreSQL server. (Otherwise, generated when postgresql\_should\_generate\_admin\_password is true). | `string` | `null` | no | -| postgresql\_admin\_username | Administrator username for PostgreSQL server | `string` | `"pgadmin"` | no | -| postgresql\_databases | Map of databases to create with collation and charset | ```map(object({ collation = string charset = string }))``` | `null` | no | -| postgresql\_delegated\_subnet\_id | Subnet ID with delegation to Microsoft.DBforPostgreSQL/flexibleServers | `string` | `null` | no | -| postgresql\_should\_enable\_extensions | Whether to enable PostgreSQL extensions via azure.extensions | `bool` | `true` | no | -| postgresql\_should\_enable\_geo\_redundant\_backup | Whether to enable geo-redundant backups for PostgreSQL | `bool` | `false` | no | -| postgresql\_should\_enable\_timescaledb | Whether to enable TimescaleDB extension for PostgreSQL | `bool` | `true` | no | -| postgresql\_should\_generate\_admin\_password | Whether to auto-generate PostgreSQL admin password. | `bool` | `true` | no | -| postgresql\_should\_store\_credentials\_in\_key\_vault | Whether to store PostgreSQL admin credentials in Key Vault. | `bool` | `true` | no | -| postgresql\_sku\_name | SKU name for PostgreSQL server | `string` | `"GP_Standard_D2s_v3"` | no | -| postgresql\_storage\_mb | Storage size in megabytes for PostgreSQL | `number` | `32768` | no | -| postgresql\_version | PostgreSQL server version | `string` | `"16"` | no | -| redis\_clustering\_policy | Clustering policy for Redis cache (OSSCluster or EnterpriseCluster) | `string` | `"OSSCluster"` | no | -| redis\_should\_enable\_high\_availability | Whether to enable high availability for Redis cache | `bool` | `true` | no | -| redis\_sku\_name | SKU name for Azure Managed Redis cache | `string` | `"Balanced_B10"` | no | -| resolver\_subnet\_address\_prefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets | `string` | `"10.0.9.0/28"` | no | -| resource\_group\_name | Name of the resource group to create or use. Otherwise, 'rg-{resource\_prefix}-{environment}-{instance}' | `string` | `null` | no | -| should\_add\_current\_user\_cluster\_admin | Whether to give the current signed-in user cluster-admin permissions on the new cluster | `bool` | `true` | no | -| should\_create\_aks | Whether to deploy Azure Kubernetes Service | `bool` | `false` | no | -| should\_create\_aks\_identity | Whether to create a user-assigned identity for the AKS cluster when using custom private DNS zones | `bool` | `false` | no | -| should\_create\_anonymous\_broker\_listener | Whether to enable an insecure anonymous AIO MQ broker listener; use only for dev or test environments | `bool` | `false` | no | -| should\_create\_azure\_functions | Whether to create the Azure Functions resources including the App Service plan | `bool` | `false` | no | -| should\_deploy\_ai\_foundry | Whether to deploy Azure AI Foundry resources | `bool` | `false` | no | -| should\_deploy\_azureml | Whether to deploy the Azure Machine Learning workspace and optional compute cluster | `bool` | `false` | no | -| should\_deploy\_edge\_azureml | Whether to deploy the Azure Machine Learning edge extension when Azure ML is enabled | `bool` | `false` | no | -| should\_deploy\_postgresql | Whether to deploy PostgreSQL Flexible Server component | `bool` | `false` | no | -| should\_deploy\_redis | Whether to deploy Azure Managed Redis component | `bool` | `false` | no | -| should\_deploy\_resource\_sync\_rules | Whether to deploy resource sync rules | `bool` | `false` | no | -| should\_enable\_akri\_media\_connector | Whether to deploy the Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_akri\_onvif\_connector | Whether to deploy the Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_akri\_rest\_connector | Whether to deploy the Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_akri\_sse\_connector | Whether to deploy the Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_key\_vault\_public\_network\_access | Whether to enable public network access for the Key Vault | `bool` | `true` | no | -| should\_enable\_managed\_outbound\_access | Whether to enable managed outbound egress via NAT gateway instead of platform default internet access | `bool` | `true` | no | -| should\_enable\_oidc\_issuer | Whether to enable the OIDC issuer URL for the cluster | `bool` | `true` | no | -| should\_enable\_opc\_ua\_simulator | Whether to deploy the OPC UA simulator to the cluster | `bool` | `false` | no | -| should\_enable\_otel\_collector | Whether to deploy the OpenTelemetry Collector and Azure Monitor ConfigMap | `bool` | `true` | no | -| should\_enable\_private\_endpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources so Prometheus ingestion remains on private link | `bool` | `false` | no | -| should\_enable\_private\_resolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints | `bool` | `false` | no | -| should\_enable\_storage\_public\_network\_access | Whether to enable public network access for the storage account | `bool` | `true` | no | -| should\_enable\_vpn\_gateway | Whether to create a VPN gateway for secure access to private endpoints | `bool` | `false` | no | -| should\_enable\_workload\_identity | Whether to enable Azure AD workload identity for the cluster | `bool` | `true` | no | -| should\_get\_custom\_locations\_oid | Whether to get the Custom Locations object ID using Terraform's azuread provider Otherwise, provide 'custom\_locations\_oid' or rely on `az connectedk8s enable-features` during cluster setup | `bool` | `true` | no | -| should\_use\_arc\_machines | Whether to orchestrate the cluster using existing Arc-enabled servers instead of deploying Azure virtual machines | `bool` | `false` | no | -| storage\_account\_is\_hns\_enabled | Whether to enable hierarchical namespace on the storage account when Azure Machine Learning is not deployed; automatically forced to false when should\_deploy\_azureml is true | `bool` | `true` | no | -| subnet\_address\_prefixes\_aks | Address prefixes for the AKS subnet | `list(string)` | ```[ "10.0.4.0/24" ]``` | no | -| subnet\_address\_prefixes\_aks\_pod | Address prefixes for the AKS pod subnet | `list(string)` | ```[ "10.0.5.0/24" ]``` | no | -| use\_existing\_resource\_group | Whether to use an existing resource group with the provided or computed name instead of creating a new one | `bool` | `false` | no | -| vpn\_gateway\_config | VPN gateway configuration including SKU, generation, client address pool, and supported protocols | ```object({ sku = optional(string, "VpnGw1") generation = optional(string, "Generation1") client_address_pool = optional(list(string), ["192.168.200.0/24"]) protocols = optional(list(string), ["OpenVPN", "IkeV2"]) })``` | `{}` | no | -| vpn\_gateway\_should\_generate\_ca | Whether to generate a new CA certificate; when false, uses an existing certificate from Key Vault | `bool` | `true` | no | -| vpn\_gateway\_should\_use\_azure\_ad\_auth | Whether to use Azure AD authentication for the VPN gateway; otherwise, certificate authentication is used | `bool` | `true` | no | -| vpn\_gateway\_subnet\_address\_prefixes | Address prefixes for the GatewaySubnet; must be /27 or larger | `list(string)` | ```[ "10.0.2.0/27" ]``` | no | -| vpn\_site\_connections | Site-to-site VPN connection definitions. Provide on-premises address spaces that do not overlap with Azure VNets | ```list(object({ name = string address_spaces = list(string) shared_key_reference = string connection_mode = optional(string, "Default") dpd_timeout_seconds = optional(number) gateway_fqdn = optional(string) gateway_ip_address = optional(string) ike_protocol = optional(string, "IKEv2") use_policy_based_selectors = optional(bool, false) bgp_settings = optional(object({ asn = number peer_address = string peer_weight = optional(number) })) ipsec_policy = optional(object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })) }))``` | `[]` | no | -| vpn\_site\_default\_ipsec\_policy | Fallback IPsec parameters applied when site definitions omit ipsec\_policy | ```object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })``` | `null` | no | -| vpn\_site\_shared\_keys | Pre-shared keys for site connections keyed by shared\_key\_reference. Manage values in secure secret stores | `map(string)` | `{}` | no | +| Name | Description | Type | Default | Required | +|------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|:--------:| +| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes | +| location | Location for all resources in this module | `string` | n/a | yes | +| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes | +| acr\_allow\_trusted\_services | Whether trusted Azure services can bypass ACR network rules | `bool` | `true` | no | +| acr\_allowed\_public\_ip\_ranges | CIDR ranges permitted to reach the ACR public endpoint | `list(string)` | `[]` | no | +| acr\_data\_endpoint\_enabled | Whether to enable the dedicated ACR data endpoint | `bool` | `true` | no | +| acr\_public\_network\_access\_enabled | Whether to enable the ACR public endpoint alongside private connectivity | `bool` | `false` | no | +| acr\_sku | SKU name for the Azure Container Registry | `string` | `"Premium"` | no | +| ai\_foundry\_model\_deployments | Map of model deployments for AI Foundry | ```map(object({ name = string model = object({ format = string name = string version = string }) scale = object({ type = string capacity = number }) rai_policy_name = optional(string) version_upgrade_option = optional(string, "OnceNewDefaultVersionAvailable") }))``` | `{}` | no | +| ai\_foundry\_private\_dns\_zone\_ids | List of private DNS zone IDs for the AI Foundry private endpoint | `list(string)` | `[]` | no | +| ai\_foundry\_projects | Map of AI Foundry projects to create. SKU defaults to 'S0' (currently the only supported value) | ```map(object({ name = string display_name = string description = string sku = optional(string, "S0") }))``` | `{}` | no | +| ai\_foundry\_rai\_policies | Map of Responsible AI (RAI) content filtering policies. Must be created before referenced in model deployments. | ```map(object({ name = string base_policy_name = optional(string, "Microsoft.Default") mode = optional(string, "Blocking") content_filters = optional(list(object({ name = string enabled = optional(bool, true) blocking = optional(bool, true) severity_threshold = optional(string, "Medium") source = string })), []) }))``` | `{}` | no | +| ai\_foundry\_should\_enable\_local\_auth | Whether to enable local (API key) authentication for AI Foundry | `bool` | `true` | no | +| ai\_foundry\_should\_enable\_private\_endpoint | Whether to enable private endpoint for AI Foundry | `bool` | `false` | no | +| ai\_foundry\_should\_enable\_public\_network\_access | Whether to enable public network access to AI Foundry | `bool` | `true` | no | +| ai\_foundry\_sku | SKU name for the AI Foundry account | `string` | `"S0"` | no | +| aio\_features | AIO instance features with mode ('Stable', 'Preview', 'Disabled') and settings ('Enabled', 'Disabled') | ```map(object({ mode = optional(string) settings = optional(map(string)) }))``` | `null` | no | +| aks\_private\_dns\_zone\_id | ID of the private DNS zone to use for AKS private cluster. Use 'system', 'none', or a resource ID | `string` | `null` | no | +| aks\_should\_enable\_private\_cluster | Whether to enable private cluster mode for AKS | `bool` | `false` | no | +| aks\_should\_enable\_private\_cluster\_public\_fqdn | Whether to create a private cluster public FQDN for AKS | `bool` | `false` | no | +| arc\_machine\_count | Number of Arc-enabled machines to target for the cluster when should\_use\_arc\_machines is true | `number` | `1` | no | +| arc\_machine\_name\_prefix | Prefix for the Arc-enabled machine names; otherwise resource\_prefix when should\_use\_arc\_machines is true | `string` | `null` | no | +| arc\_machine\_resource\_group\_name | Resource group name that contains the Arc-enabled servers when should\_use\_arc\_machines is true | `string` | `null` | no | +| azureml\_ml\_workload\_subjects | Custom Kubernetes service account subjects for AzureML workload federation. Example: ['system:serviceaccount:azureml:azureml-workload', 'system:serviceaccount:osmo:osmo-workload'] | `list(string)` | `null` | no | +| azureml\_registry\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning registry when deployed | `bool` | `true` | no | +| azureml\_should\_create\_compute\_cluster | Whether to create a compute cluster for Azure Machine Learning training workloads | `bool` | `true` | no | +| azureml\_should\_create\_ml\_workload\_identity | Whether to create a user-assigned managed identity for AzureML workload federation. | `bool` | `false` | no | +| azureml\_should\_deploy\_registry | Whether to deploy Azure Machine Learning registry resources alongside the workspace | `bool` | `false` | no | +| azureml\_should\_enable\_private\_endpoint | Whether to enable a private endpoint for the Azure Machine Learning workspace | `bool` | `false` | no | +| azureml\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning workspace | `bool` | `true` | no | +| certificate\_subject | Certificate subject information for auto-generated certificates | ```object({ common_name = optional(string, "Full Multi Node VPN Gateway Root Certificate") organization = optional(string, "Edge AI Accelerator") organizational_unit = optional(string, "IT") country = optional(string, "US") province = optional(string, "WA") locality = optional(string, "Redmond") })``` | `{}` | no | +| certificate\_validity\_days | Validity period in days for auto-generated certificates | `number` | `365` | no | +| cluster\_server\_host\_machine\_username | Username for the Arc or VM host machines that receive kube-config during setup Otherwise, resource\_prefix when the user exists on the machine | `string` | `null` | no | +| cluster\_server\_ip | IP address for the cluster server used by node machines when should\_use\_arc\_machines is true | `string` | `null` | no | +| custom\_akri\_connectors | List of custom Akri connector templates with user-defined endpoint types and container images. Supports built-in types (rest, media, onvif, sse) or custom types with custom\_endpoint\_type and custom\_image\_name. Built-in connectors default to mcr.microsoft.com/azureiotoperations/akri-connectors/connector\_type:0.5.1. | ```list(object({ name = string type = string // "rest", "media", "onvif", "sse", "custom" // Custom Connector Fields (required when type = "custom") custom_endpoint_type = optional(string) // e.g., "Contoso.Modbus", "Acme.CustomProtocol" custom_image_name = optional(string) // e.g., "my_acr.azurecr.io/custom-connector" custom_endpoint_version = optional(string, "1.0") // Runtime Configuration (defaults applied based on connector type) registry = optional(string) // Defaults: mcr.microsoft.com for built-in types image_tag = optional(string) // Defaults: 0.5.1 for built-in types, latest for custom replicas = optional(number, 1) image_pull_policy = optional(string) // Default: IfNotPresent // Diagnostics log_level = optional(string) // Default: info (lowercase: trace, debug, info, warning, error, critical) // MQTT Override (uses shared config if not provided) mqtt_config = optional(object({ host = string audience = string ca_configmap = string keep_alive_seconds = optional(number, 60) max_inflight_messages = optional(number, 100) session_expiry_seconds = optional(number, 600) })) // Optional Advanced Fields aio_min_version = optional(string) aio_max_version = optional(string) allocation = optional(object({ policy = string // "Bucketized" bucket_size = number // 1-100 })) additional_configuration = optional(map(string)) secrets = optional(list(object({ secret_alias = string secret_key = string secret_ref = string }))) trust_settings = optional(object({ trust_list_secret_ref = string })) }))``` | `[]` | no | +| custom\_locations\_oid | The object id of the Custom Locations Entra ID application for your tenant If none is provided, the script attempts to retrieve this value which requires 'Application.Read.All' or 'Directory.Read.All' permissions ```sh az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv``` | `string` | `null` | no | +| dns\_prefix | DNS prefix for the AKS cluster. When null a value is generated | `string` | `null` | no | +| enable\_auto\_scaling | Whether to enable auto-scaling for the default node pool | `bool` | `false` | no | +| existing\_certificate\_name | Name of the existing certificate in Key Vault when vpn\_gateway\_should\_generate\_ca is false | `string` | `null` | no | +| host\_machine\_count | Number of edge host virtual machines to create for the multi-node cluster | `number` | `3` | no | +| instance | Instance identifier for naming resources: 001, 002, etc | `string` | `"001"` | no | +| max\_count | Maximum node count for the default node pool | `number` | `null` | no | +| min\_count | Minimum node count for the default node pool | `number` | `null` | no | +| namespaced\_assets | List of namespaced assets with enhanced configuration support | ```list(object({ name = string display_name = optional(string) device_ref = optional(object({ device_name = string endpoint_name = string })) asset_endpoint_profile_ref = optional(string) default_datasets_configuration = optional(string) default_streams_configuration = optional(string) default_events_configuration = optional(string) description = optional(string) documentation_uri = optional(string) enabled = optional(bool, true) hardware_revision = optional(string) manufacturer = optional(string) manufacturer_uri = optional(string) model = optional(string) product_code = optional(string) serial_number = optional(string) software_revision = optional(string) attributes = optional(map(string), {}) datasets = optional(list(object({ name = string data_points = list(object({ data_point_configuration = optional(string) data_source = string name = string observability_mode = optional(string) rest_sampling_interval_ms = optional(number) rest_mqtt_topic = optional(string) rest_include_state_store = optional(bool) rest_state_store_key = optional(string) })) dataset_configuration = optional(string) data_source = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) type_ref = optional(string) })), []) streams = optional(list(object({ name = string stream_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })), []) event_groups = optional(list(object({ name = string data_source = optional(string) event_group_configuration = optional(string) type_ref = optional(string) default_destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) events = list(object({ name = string data_source = string event_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })) })), []) management_groups = optional(list(object({ name = string data_source = optional(string) management_group_configuration = optional(string) type_ref = optional(string) default_topic = optional(string) default_timeout_in_seconds = optional(number, 100) actions = list(object({ name = string action_type = string target_uri = string topic = optional(string) timeout_in_seconds = optional(number) action_configuration = optional(string) type_ref = optional(string) })) })), []) }))``` | `[]` | no | +| namespaced\_devices | List of namespaced devices to create; otherwise, an empty list | ```list(object({ name = string enabled = optional(bool, true) endpoints = object({ outbound = optional(object({ assigned = object({}) }), { assigned = {} }) inbound = map(object({ endpoint_type = string address = string version = optional(string, null) additionalConfiguration = optional(string) authentication = object({ method = string usernamePasswordCredentials = optional(object({ usernameSecretName = string passwordSecretName = string })) x509Credentials = optional(object({ certificateSecretName = string })) }) trustSettings = optional(object({ trustList = string })) })) }) }))``` | `[]` | no | +| nat\_gateway\_idle\_timeout\_minutes | Idle timeout in minutes for NAT gateway connections | `number` | `4` | no | +| nat\_gateway\_public\_ip\_count | Number of public IP addresses to associate with the NAT gateway (example: 2) | `number` | `1` | no | +| nat\_gateway\_zones | Availability zones for NAT gateway resources when zone redundancy is required (example: ['1','2']) | `list(string)` | `[]` | no | +| node\_count | Number of nodes for the agent pool in the AKS cluster | `number` | `1` | no | +| node\_pools | Additional node pools for the AKS cluster; map key is used as the node pool name | ```map(object({ node_count = number vm_size = string subnet_address_prefixes = list(string) pod_subnet_address_prefixes = list(string) node_taints = optional(list(string), []) enable_auto_scaling = optional(bool, false) min_count = optional(number, null) max_count = optional(number, null) }))``` | `{}` | no | +| node\_vm\_size | VM size for the agent pool in the AKS cluster | `string` | `"Standard_D8ds_v5"` | no | +| postgresql\_admin\_password | Administrator password for PostgreSQL server. (Otherwise, generated when postgresql\_should\_generate\_admin\_password is true). | `string` | `null` | no | +| postgresql\_admin\_username | Administrator username for PostgreSQL server | `string` | `"pgadmin"` | no | +| postgresql\_databases | Map of databases to create with collation and charset | ```map(object({ collation = string charset = string }))``` | `null` | no | +| postgresql\_delegated\_subnet\_id | Subnet ID with delegation to Microsoft.DBforPostgreSQL/flexibleServers | `string` | `null` | no | +| postgresql\_should\_enable\_extensions | Whether to enable PostgreSQL extensions via azure.extensions | `bool` | `true` | no | +| postgresql\_should\_enable\_geo\_redundant\_backup | Whether to enable geo-redundant backups for PostgreSQL | `bool` | `false` | no | +| postgresql\_should\_enable\_timescaledb | Whether to enable TimescaleDB extension for PostgreSQL | `bool` | `true` | no | +| postgresql\_should\_generate\_admin\_password | Whether to auto-generate PostgreSQL admin password. | `bool` | `true` | no | +| postgresql\_should\_store\_credentials\_in\_key\_vault | Whether to store PostgreSQL admin credentials in Key Vault. | `bool` | `true` | no | +| postgresql\_sku\_name | SKU name for PostgreSQL server | `string` | `"GP_Standard_D2s_v3"` | no | +| postgresql\_storage\_mb | Storage size in megabytes for PostgreSQL | `number` | `32768` | no | +| postgresql\_version | PostgreSQL server version | `string` | `"16"` | no | +| redis\_clustering\_policy | Clustering policy for Redis cache (OSSCluster or EnterpriseCluster) | `string` | `"OSSCluster"` | no | +| redis\_should\_enable\_high\_availability | Whether to enable high availability for Redis cache | `bool` | `true` | no | +| redis\_sku\_name | SKU name for Azure Managed Redis cache | `string` | `"Balanced_B10"` | no | +| registry\_endpoints | List of additional container registry endpoints for pulling custom artifacts (WASM modules, graph definitions, connector templates). MCR (mcr.microsoft.com) is always added automatically with anonymous authentication. The `acr_resource_id` field enables automatic AcrPull role assignment for ACR endpoints using SystemAssignedManagedIdentity authentication. When `should_assign_acr_pull_for_aio` is true and `acr_resource_id` is provided, the AIO extension's identity will be granted AcrPull access to the specified ACR. | ```list(object({ name = string host = string acr_resource_id = optional(string) should_assign_acr_pull_for_aio = optional(bool, false) authentication = object({ method = string system_assigned_managed_identity_settings = optional(object({ audience = optional(string) })) user_assigned_managed_identity_settings = optional(object({ client_id = string tenant_id = string scope = optional(string) })) artifact_pull_secret_settings = optional(object({ secret_ref = string })) }) }))``` | `[]` | no | +| resolver\_subnet\_address\_prefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets | `string` | `"10.0.9.0/28"` | no | +| resource\_group\_name | Name of the resource group to create or use. Otherwise, 'rg-{resource\_prefix}-{environment}-{instance}' | `string` | `null` | no | +| should\_add\_current\_user\_cluster\_admin | Whether to give the current signed-in user cluster-admin permissions on the new cluster | `bool` | `true` | no | +| should\_create\_aks | Whether to deploy Azure Kubernetes Service | `bool` | `false` | no | +| should\_create\_aks\_identity | Whether to create a user-assigned identity for the AKS cluster when using custom private DNS zones | `bool` | `false` | no | +| should\_create\_anonymous\_broker\_listener | Whether to enable an insecure anonymous AIO MQ broker listener; use only for dev or test environments | `bool` | `false` | no | +| should\_create\_azure\_functions | Whether to create the Azure Functions resources including the App Service plan | `bool` | `false` | no | +| should\_deploy\_ai\_foundry | Whether to deploy Azure AI Foundry resources | `bool` | `false` | no | +| should\_deploy\_azureml | Whether to deploy the Azure Machine Learning workspace and optional compute cluster | `bool` | `false` | no | +| should\_deploy\_edge\_azureml | Whether to deploy the Azure Machine Learning edge extension when Azure ML is enabled | `bool` | `false` | no | +| should\_deploy\_postgresql | Whether to deploy PostgreSQL Flexible Server component | `bool` | `false` | no | +| should\_deploy\_redis | Whether to deploy Azure Managed Redis component | `bool` | `false` | no | +| should\_deploy\_resource\_sync\_rules | Whether to deploy resource sync rules | `bool` | `false` | no | +| should\_enable\_akri\_media\_connector | Whether to deploy the Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_akri\_onvif\_connector | Whether to deploy the Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_akri\_rest\_connector | Whether to deploy the Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_akri\_sse\_connector | Whether to deploy the Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_key\_vault\_public\_network\_access | Whether to enable public network access for the Key Vault | `bool` | `true` | no | +| should\_enable\_managed\_outbound\_access | Whether to enable managed outbound egress via NAT gateway instead of platform default internet access | `bool` | `true` | no | +| should\_enable\_oidc\_issuer | Whether to enable the OIDC issuer URL for the cluster | `bool` | `true` | no | +| should\_enable\_opc\_ua\_simulator | Whether to deploy the OPC UA simulator to the cluster | `bool` | `false` | no | +| should\_enable\_otel\_collector | Whether to deploy the OpenTelemetry Collector and Azure Monitor ConfigMap | `bool` | `true` | no | +| should\_enable\_private\_endpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources so Prometheus ingestion remains on private link | `bool` | `false` | no | +| should\_enable\_private\_resolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints | `bool` | `false` | no | +| should\_enable\_storage\_public\_network\_access | Whether to enable public network access for the storage account | `bool` | `true` | no | +| should\_enable\_vpn\_gateway | Whether to create a VPN gateway for secure access to private endpoints | `bool` | `false` | no | +| should\_enable\_workload\_identity | Whether to enable Azure AD workload identity for the cluster | `bool` | `true` | no | +| should\_get\_custom\_locations\_oid | Whether to get the Custom Locations object ID using Terraform's azuread provider Otherwise, provide 'custom\_locations\_oid' or rely on `az connectedk8s enable-features` during cluster setup | `bool` | `true` | no | +| should\_include\_acr\_registry\_endpoint | Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication | `bool` | `false` | no | +| should\_use\_arc\_machines | Whether to orchestrate the cluster using existing Arc-enabled servers instead of deploying Azure virtual machines | `bool` | `false` | no | +| storage\_account\_is\_hns\_enabled | Whether to enable hierarchical namespace on the storage account when Azure Machine Learning is not deployed; automatically forced to false when should\_deploy\_azureml is true | `bool` | `true` | no | +| subnet\_address\_prefixes\_aks | Address prefixes for the AKS subnet | `list(string)` | ```[ "10.0.4.0/24" ]``` | no | +| subnet\_address\_prefixes\_aks\_pod | Address prefixes for the AKS pod subnet | `list(string)` | ```[ "10.0.5.0/24" ]``` | no | +| use\_existing\_resource\_group | Whether to use an existing resource group with the provided or computed name instead of creating a new one | `bool` | `false` | no | +| vpn\_gateway\_config | VPN gateway configuration including SKU, generation, client address pool, and supported protocols | ```object({ sku = optional(string, "VpnGw1") generation = optional(string, "Generation1") client_address_pool = optional(list(string), ["192.168.200.0/24"]) protocols = optional(list(string), ["OpenVPN", "IkeV2"]) })``` | `{}` | no | +| vpn\_gateway\_should\_generate\_ca | Whether to generate a new CA certificate; when false, uses an existing certificate from Key Vault | `bool` | `true` | no | +| vpn\_gateway\_should\_use\_azure\_ad\_auth | Whether to use Azure AD authentication for the VPN gateway; otherwise, certificate authentication is used | `bool` | `true` | no | +| vpn\_gateway\_subnet\_address\_prefixes | Address prefixes for the GatewaySubnet; must be /27 or larger | `list(string)` | ```[ "10.0.2.0/27" ]``` | no | +| vpn\_site\_connections | Site-to-site VPN connection definitions. Provide on-premises address spaces that do not overlap with Azure VNets | ```list(object({ name = string address_spaces = list(string) shared_key_reference = string connection_mode = optional(string, "Default") dpd_timeout_seconds = optional(number) gateway_fqdn = optional(string) gateway_ip_address = optional(string) ike_protocol = optional(string, "IKEv2") use_policy_based_selectors = optional(bool, false) bgp_settings = optional(object({ asn = number peer_address = string peer_weight = optional(number) })) ipsec_policy = optional(object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })) }))``` | `[]` | no | +| vpn\_site\_default\_ipsec\_policy | Fallback IPsec parameters applied when site definitions omit ipsec\_policy | ```object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })``` | `null` | no | +| vpn\_site\_shared\_keys | Pre-shared keys for site connections keyed by shared\_key\_reference. Manage values in secure secret stores | `map(string)` | `{}` | no | ## Outputs diff --git a/blueprints/full-multi-node-cluster/terraform/main.tf b/blueprints/full-multi-node-cluster/terraform/main.tf index 0bbc0c76..6c1827e3 100644 --- a/blueprints/full-multi-node-cluster/terraform/main.tf +++ b/blueprints/full-multi-node-cluster/terraform/main.tf @@ -12,6 +12,21 @@ locals { vm_host_virtual_machines = try(module.cloud_vm_host[0].virtual_machines, []) cluster_machine_count = local.should_use_arc_machines ? var.arc_machine_count : var.host_machine_count cluster_node_machine_count = max(local.cluster_machine_count - 1, 0) + + acr_registry_endpoint = var.should_include_acr_registry_endpoint ? [{ + name = "acr-${var.resource_prefix}" + host = "${module.cloud_acr.acr.name}.azurecr.io" + acr_resource_id = module.cloud_acr.acr.id + should_assign_acr_pull_for_aio = true + authentication = { + method = "SystemAssignedManagedIdentity" + system_assigned_managed_identity_settings = null + user_assigned_managed_identity_settings = null + artifact_pull_secret_settings = null + } + }] : [] + + combined_registry_endpoints = concat(var.registry_endpoints, local.acr_registry_endpoint) } /* @@ -442,6 +457,7 @@ module "edge_iot_ops" { should_enable_akri_onvif_connector = var.should_enable_akri_onvif_connector should_enable_akri_sse_connector = var.should_enable_akri_sse_connector custom_akri_connectors = var.custom_akri_connectors + registry_endpoints = local.combined_registry_endpoints should_enable_otel_collector = var.should_enable_otel_collector } diff --git a/blueprints/full-multi-node-cluster/terraform/variables.tf b/blueprints/full-multi-node-cluster/terraform/variables.tf index 51565954..975d6552 100644 --- a/blueprints/full-multi-node-cluster/terraform/variables.tf +++ b/blueprints/full-multi-node-cluster/terraform/variables.tf @@ -1004,6 +1004,100 @@ variable "custom_akri_connectors" { } } +variable "registry_endpoints" { + type = list(object({ + name = string + host = string + acr_resource_id = optional(string) + should_assign_acr_pull_for_aio = optional(bool, false) + authentication = object({ + method = string + system_assigned_managed_identity_settings = optional(object({ + audience = optional(string) + })) + user_assigned_managed_identity_settings = optional(object({ + client_id = string + tenant_id = string + scope = optional(string) + })) + artifact_pull_secret_settings = optional(object({ + secret_ref = string + })) + }) + })) + + default = [] + description = <<-EOT + List of additional container registry endpoints for pulling custom artifacts (WASM modules, graph definitions, connector templates). + MCR (mcr.microsoft.com) is always added automatically with anonymous authentication. + + The `acr_resource_id` field enables automatic AcrPull role assignment for ACR endpoints + using SystemAssignedManagedIdentity authentication. When `should_assign_acr_pull_for_aio` is true + and `acr_resource_id` is provided, the AIO extension's identity will be granted AcrPull access to the specified ACR. + EOT + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + can(regex("^[a-z0-9][a-z0-9-]*[a-z0-9]$", ep.name)) && length(ep.name) >= 3 && length(ep.name) <= 63 + ]) + error_message = "Registry endpoint name must be 3-63 characters, contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + contains(["SystemAssignedManagedIdentity", "UserAssignedManagedIdentity", "ArtifactPullSecret", "Anonymous"], ep.authentication.method) + ]) + error_message = "Authentication method must be one of: SystemAssignedManagedIdentity, UserAssignedManagedIdentity, ArtifactPullSecret, Anonymous" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.authentication.method != "UserAssignedManagedIdentity" || ( + ep.authentication.user_assigned_managed_identity_settings != null && + ep.authentication.user_assigned_managed_identity_settings.client_id != null && + ep.authentication.user_assigned_managed_identity_settings.tenant_id != null + ) + ]) + error_message = "UserAssignedManagedIdentity authentication requires client_id and tenant_id in user_assigned_managed_identity_settings" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.authentication.method != "ArtifactPullSecret" || ( + ep.authentication.artifact_pull_secret_settings != null && + ep.authentication.artifact_pull_secret_settings.secret_ref != null + ) + ]) + error_message = "ArtifactPullSecret authentication requires secret_ref in artifact_pull_secret_settings" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.name != "mcr" && ep.name != "default" + ]) + error_message = "Registry endpoint names 'mcr' and 'default' are reserved" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.acr_resource_id == null || ep.authentication.method == "SystemAssignedManagedIdentity" + ]) + error_message = "acr_resource_id can only be specified with SystemAssignedManagedIdentity authentication method" + } +} + +variable "should_include_acr_registry_endpoint" { + type = bool + default = false + description = "Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication" +} + # ------------------------------------------------------------------------------ # AI Foundry Variables # ------------------------------------------------------------------------------ diff --git a/blueprints/full-single-node-cluster/README.md b/blueprints/full-single-node-cluster/README.md index c9ca317e..83b45b26 100644 --- a/blueprints/full-single-node-cluster/README.md +++ b/blueprints/full-single-node-cluster/README.md @@ -239,6 +239,54 @@ See [rest-connector-assets.tfvars.example](terraform/rest-connector-assets.tfvar Follow detailed deployment instructions from the blueprints README.md, [Detailed Deployment Workflow](../README.md#detailed-deployment-workflow) +## Testing and Validation + +This blueprint includes comprehensive test suites for validating deployments and maintaining code quality. + +### Test Directory + +**Location:** [tests/](tests/) + +The tests directory contains contract tests and end-to-end deployment validation for both Terraform and Bicep implementations. + +**Key Components:** + +- **Contract Tests** - Fast static validation ensuring output declarations match test expectations (zero cost, no Azure resources) +- **Deployment Tests** - Full end-to-end deployment validation with infrastructure creation and functional testing +- **Helper Scripts** - `run-contract-tests.sh` and `run-deployment-tests.sh` for simplified test execution + +**See:** [tests/README.md](tests/README.md) for complete testing documentation including setup, usage, and troubleshooting + +### For Maintainers + +When modifying this blueprint: + +#### Before Making Changes + +1. **Run contract tests** to establish baseline: `cd tests && ./run-contract-tests.sh both` +2. **Review test structure** in [tests/outputs.go](tests/outputs.go) to understand output contract + +#### After Making Changes + +1. **Update output contract** if adding/removing/renaming outputs: + - Update struct fields in [tests/outputs.go](tests/outputs.go) + - Update framework configurations: [terraform/outputs.tf](terraform/outputs.tf) or [bicep/main.bicep](bicep/main.bicep) + +2. **Run contract tests** to verify declarations: `cd tests && ./run-contract-tests.sh both` + +3. **Update deployment tests** if changing deployment behavior: + - Review [tests/deploy_terraform_test.go](tests/deploy_terraform_test.go) or [tests/deploy_bicep_test.go](tests/deploy_bicep_test.go) + - Update validation logic in [tests/validation.go](tests/validation.go) if needed + +4. **Run deployment tests** to validate changes: `cd tests && ./run-deployment-tests.sh ` + - Valid framework values: 'terraform', 'bicep', or 'both' + - Set `CLEANUP_RESOURCES=true` to auto-delete resources after testing + - Expected duration: 30-45 minutes for full deployment + +5. **Update documentation** if changing parameters, modules, or architecture: + - Update this README.md with new parameters or module references + - Update [tests/README.md](tests/README.md) if test behavior changes + --- diff --git a/blueprints/full-single-node-cluster/bicep/README.md b/blueprints/full-single-node-cluster/bicep/README.md index 21b8159a..0de23241 100644 --- a/blueprints/full-single-node-cluster/bicep/README.md +++ b/blueprints/full-single-node-cluster/bicep/README.md @@ -7,49 +7,51 @@ Deploys a complete end-to-end environment for Azure IoT Operations on a single-n ## Parameters -| Name | Description | Type | Default | Required | -|:----------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:---------| -| common | The common component configuration. | `[_5.Common](#user-defined-types)` | n/a | yes | -| resourceGroupName | The name for the resource group. If not provided, a default name will be generated. | `string` | [format('rg-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| useExistingResourceGroup | Whether to use an existing resource group instead of creating a new one. | `bool` | `false` | no | -| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | -| adminPassword | Password used for the host VM. | `securestring` | n/a | yes | -| shouldCreateAcrPrivateEndpoint | Whether to create a private endpoint for the Azure Container Registry. | `bool` | `false` | no | -| shouldDeployAiFoundry | Whether to deploy AI Foundry resources. | `bool` | `false` | no | -| aiFoundryConfig | The AI Foundry configuration settings. | `[_2.AiFoundryConfig](#user-defined-types)` | [variables('_2.aiFoundryConfigDefaults')] | no | -| aiFoundryProjects | Array of AI Foundry projects to create. | `array` | [] | no | -| aiFoundryRaiPolicies | Array of RAI policies to create. | `array` | [] | no | -| aiFoundryModelDeployments | Array of model deployments to create. | `array` | [] | no | -| shouldCreateAiFoundryPrivateEndpoint | Whether to create a private endpoint for AI Foundry. | `bool` | `false` | no | -| shouldEnableManagedOutboundAccess | Whether to enable managed outbound access via NAT gateway instead of platform default internet access. | `bool` | `true` | no | -| natGatewayPublicIpCount | Number of public IP addresses for NAT Gateway (1-16). | `int` | 1 | no | -| natGatewayIdleTimeoutMinutes | Idle timeout in minutes for NAT gateway connections (4-120). | `int` | 4 | no | -| natGatewayZones | Availability zones for NAT Gateway. Empty array for regional deployment. | `array` | [] | no | -| shouldEnablePrivateEndpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources. | `bool` | `false` | no | -| shouldEnablePrivateResolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints. | `bool` | `false` | no | -| resolverSubnetAddressPrefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets. | `string` | 10.0.9.0/28 | no | -| shouldEnableKeyVaultPublicNetworkAccess | Whether to enable public network access for the Key Vault. | `bool` | `true` | no | -| shouldEnableStoragePublicNetworkAccess | Whether to enable public network access for the storage account. | `bool` | `true` | no | -| subnetAddressPrefixAcr | Address prefix for the ACR subnet. | `string` | 10.0.4.0/24 | no | -| subnetAddressPrefixAks | Address prefix for the AKS subnet. | `string` | 10.0.5.0/24 | no | -| subnetAddressPrefixAksPod | Address prefix for the AKS pod subnet. | `string` | 10.0.6.0/24 | no | -| shouldEnableVpnGateway | Whether to deploy VPN Gateway for remote access. | `bool` | `false` | no | -| vpnGatewayConfig | VPN Gateway configuration settings. | `[_1.VpnGatewayConfig](#user-defined-types)` | [variables('_1.vpnGatewayConfigDefaults')] | no | -| vpnGatewayAzureAdConfig | Azure AD authentication configuration for VPN Gateway. | `[_1.AzureAdConfig](#user-defined-types)` | [variables('_1.azureAdConfigDefaults')] | no | -| shouldCreateAks | Whether to create an Azure Kubernetes Service cluster. | `bool` | `false` | no | -| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
Can be retrieved using:

  az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes | -| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | -| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | -| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | -| namespacedDevices | List of namespaced devices to create. | `array` | [] | no | -| assetEndpointProfiles | List of asset endpoint profiles to create. | `array` | [] | no | -| legacyAssets | List of legacy assets to create. | `array` | [] | no | -| namespacedAssets | List of namespaced assets to create. | `array` | [] | no | -| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | -| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no | +| Name | Description | Type | Default | Required | +|:----------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:---------| +| common | The common component configuration. | `[_5.Common](#user-defined-types)` | n/a | yes | +| resourceGroupName | The name for the resource group. If not provided, a default name will be generated. | `string` | [format('rg-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| useExistingResourceGroup | Whether to use an existing resource group instead of creating a new one. | `bool` | `false` | no | +| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | +| adminPassword | Password used for the host VM. | `securestring` | n/a | yes | +| shouldCreateAcrPrivateEndpoint | Whether to create a private endpoint for the Azure Container Registry. | `bool` | `false` | no | +| shouldDeployAiFoundry | Whether to deploy AI Foundry resources. | `bool` | `false` | no | +| aiFoundryConfig | The AI Foundry configuration settings. | `[_2.AiFoundryConfig](#user-defined-types)` | [variables('_2.aiFoundryConfigDefaults')] | no | +| aiFoundryProjects | Array of AI Foundry projects to create. | `[_2.AiProject](#user-defined-types)[]` | [] | no | +| aiFoundryRaiPolicies | Array of RAI policies to create. | `[_2.RaiPolicy](#user-defined-types)[]` | [] | no | +| aiFoundryModelDeployments | Array of model deployments to create. | `[_2.ModelDeployment](#user-defined-types)[]` | [] | no | +| shouldCreateAiFoundryPrivateEndpoint | Whether to create a private endpoint for AI Foundry. | `bool` | `false` | no | +| shouldEnableManagedOutboundAccess | Whether to enable managed outbound access via NAT gateway instead of platform default internet access. | `bool` | `true` | no | +| natGatewayPublicIpCount | Number of public IP addresses for NAT Gateway (1-16). | `int` | 1 | no | +| natGatewayIdleTimeoutMinutes | Idle timeout in minutes for NAT gateway connections (4-120). | `int` | 4 | no | +| natGatewayZones | Availability zones for NAT Gateway. Empty array for regional deployment. | `string[]` | [] | no | +| shouldEnablePrivateEndpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources. | `bool` | `false` | no | +| shouldEnablePrivateResolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints. | `bool` | `false` | no | +| resolverSubnetAddressPrefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets. | `string` | 10.0.9.0/28 | no | +| shouldEnableKeyVaultPublicNetworkAccess | Whether to enable public network access for the Key Vault. | `bool` | `true` | no | +| shouldEnableStoragePublicNetworkAccess | Whether to enable public network access for the storage account. | `bool` | `true` | no | +| subnetAddressPrefixAcr | Address prefix for the ACR subnet. | `string` | 10.0.4.0/24 | no | +| subnetAddressPrefixAks | Address prefix for the AKS subnet. | `string` | 10.0.5.0/24 | no | +| subnetAddressPrefixAksPod | Address prefix for the AKS pod subnet. | `string` | 10.0.6.0/24 | no | +| shouldEnableVpnGateway | Whether to deploy VPN Gateway for remote access. | `bool` | `false` | no | +| vpnGatewayConfig | VPN Gateway configuration settings. | `[_1.VpnGatewayConfig](#user-defined-types)` | [variables('_1.vpnGatewayConfigDefaults')] | no | +| vpnGatewayAzureAdConfig | Azure AD authentication configuration for VPN Gateway. | `[_1.AzureAdConfig](#user-defined-types)` | [variables('_1.azureAdConfigDefaults')] | no | +| shouldCreateAks | Whether to create an Azure Kubernetes Service cluster. | `bool` | `false` | no | +| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
Can be retrieved using:

  az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes | +| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | +| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | +| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | +| namespacedDevices | List of namespaced devices to create. | `[_4.NamespacedDevice](#user-defined-types)[]` | [] | no | +| assetEndpointProfiles | List of asset endpoint profiles to create. | `[_4.AssetEndpointProfile](#user-defined-types)[]` | [] | no | +| legacyAssets | List of legacy assets to create. | `[_4.LegacyAsset](#user-defined-types)[]` | [] | no | +| namespacedAssets | List of namespaced assets to create. | `[_4.NamespacedAsset](#user-defined-types)[]` | [] | no | +| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | +| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `[_3.AkriConnectorTemplate](#user-defined-types)[]` | [] | no | +| registryEndpoints | List of additional container registry endpoints. MCR is always added automatically. | `[_3.RegistryEndpointConfig](#user-defined-types)[]` | [] | no | +| shouldIncludeAcrRegistryEndpoint | Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication. | `bool` | `false` | no | ## Resources @@ -229,6 +231,7 @@ Deploys Azure observability resources including Azure Monitor Workspace, Log Ana | logAnalyticsName | `string` | The Log Analytics Workspace name. | | logAnalyticsId | `string` | The Log Analytics Workspace ID. | | grafanaName | `string` | The Azure Managed Grafana name. | +| grafanaEndpoint | `string` | The Azure Managed Grafana endpoint. | | metricsDataCollectionRuleName | `string` | The metrics data collection rule name. | | logsDataCollectionRuleName | `string` | The logs data collection rule name. | | monitorPrivateLinkScopeId | `string` | Azure Monitor Private Link Scope resource ID. | @@ -706,6 +709,7 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | | shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | | customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no | +| registryEndpoints | List of additional container registry endpoints for pulling custom artifacts. MCR is always added automatically. | `array` | [] | no | | akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no | | customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | | additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `array` | [] | no | @@ -736,27 +740,29 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | postInitScripts | `Microsoft.Resources/deployments` | 2025-04-01 | | iotOpsInstance | `Microsoft.Resources/deployments` | 2025-04-01 | | akriConnectors | `Microsoft.Resources/deployments` | 2025-04-01 | +| registryEndpointsModule | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScriptsSecrets | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScripts | `Microsoft.Resources/deployments` | 2025-04-01 | #### Outputs for edgeIotOps -| Name | Type | Description | -|:---------------------------|:---------|:-------------------------------------------------------------------| -| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | -| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | -| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | -| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | -| customLocationId | `string` | The ID of the deployed Custom Location. | -| customLocationName | `string` | The name of the deployed Custom Location. | -| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | -| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | -| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | -| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | -| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | -| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | +| Name | Type | Description | +|:---------------------------|:---------|:----------------------------------------------------------------------| +| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | +| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | +| aioNamespace | `string` | The namespace in the cluster where Azure IoT Operations is installed. | +| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | +| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | +| customLocationId | `string` | The ID of the deployed Custom Location. | +| customLocationName | `string` | The name of the deployed Custom Location. | +| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | +| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | +| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | +| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | +| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | +| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | ### edgeAssets @@ -1101,6 +1107,14 @@ Trust settings for Akri connector. |:-------------------|:---------|:------------------------------------| | trustListSecretRef | `string` | Reference to the trust list secret. | +### `_3.ArtifactPullSecretSettings` + +Authentication settings for Artifact Pull Secret. + +| Property | Type | Description | +|:----------|:---------|:--------------------------------------------------------------------------| +| secretRef | `string` | The name of the kubernetes secret that contains the artifact pull secret. | + ### `_3.BrokerPersistence` Broker persistence configuration for disk-backed message storage. @@ -1160,6 +1174,57 @@ The mode of the AIO instance feature. Either "Stable", "Preview" or "Disabled". The setting value of the AIO instance feature. Either "Enabled" or "Disabled". +### `_3.RegistryAuthAnonymous` + +Anonymous authentication for registry endpoint. + +| Property | Type | Description | +|:------------------|:---------|:--------------------------------------------------| +| method | `string` | Authentication method. | +| anonymousSettings | `object` | Anonymous authentication settings (empty object). | + +### `_3.RegistryAuthArtifactPullSecret` + +Artifact Pull Secret authentication for registry endpoint. + +| Property | Type | Description | +|:---------------------------|:-------------------------------------------------------|:-------------------------------| +| method | `string` | Authentication method. | +| artifactPullSecretSettings | `[_3.ArtifactPullSecretSettings](#user-defined-types)` | Artifact pull secret settings. | + +### `_3.RegistryAuthentication` + +Authentication configuration for a registry endpoint. + +### `_3.RegistryAuthSystemAssignedManagedIdentity` + +System-Assigned Managed Identity authentication for registry endpoint. + +| Property | Type | Description | +|:--------------------------------------|:------------------------------------------------------------------|:-------------------------------------------| +| method | `string` | Authentication method. | +| systemAssignedManagedIdentitySettings | `[_3.SystemAssignedManagedIdentitySettings](#user-defined-types)` | System-assigned managed identity settings. | + +### `_3.RegistryAuthUserAssignedManagedIdentity` + +User-Assigned Managed Identity authentication for registry endpoint. + +| Property | Type | Description | +|:------------------------------------|:----------------------------------------------------------------|:-----------------------------------------| +| method | `string` | Authentication method. | +| userAssignedManagedIdentitySettings | `[_3.UserAssignedManagedIdentitySettings](#user-defined-types)` | User-assigned managed identity settings. | + +### `_3.RegistryEndpointConfig` + +Container registry endpoint configuration for AIO instance. + +| Property | Type | Description | +|:---------------|:---------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------| +| name | `string` | Unique name for the registry endpoint (3-63 chars, lowercase alphanumeric and hyphens). | +| host | `string` | Container registry hostname (e.g., myregistry.azurecr.io). | +| acrResourceId | `string` | Optional ACR resource ID for automatic AcrPull role assignment. Only applicable when authentication.method is SystemAssignedManagedIdentity. | +| authentication | `[_3.RegistryAuthentication](#user-defined-types)` | Authentication configuration for the registry. | + ### `_3.Release` The common settings for Azure Arc Extensions. @@ -1213,6 +1278,14 @@ The configuration for Self-Signed Issuer for Azure IoT Operations certificates. |:------------|:---------|:------------| | trustSource | `string` | | +### `_3.SystemAssignedManagedIdentitySettings` + +Authentication settings for System-Assigned Managed Identity. + +| Property | Type | Description | +|:---------|:---------|:--------------------------------------------------------------------------------------------------------| +| audience | `string` | Audience of the service to authenticate against. Defaults to "" for ACR. | + ### `_3.TrustConfigSource` The config source of trust for how to use or generate Azure IoT Operations certificates. @@ -1236,6 +1309,16 @@ The configuration for the trust settings of Azure IoT Operations certificates. The source of trust for Azure IoT Operations certificates. +### `_3.UserAssignedManagedIdentitySettings` + +Authentication settings for User-Assigned Managed Identity. + +| Property | Type | Description | +|:---------|:---------|:---------------------------------------------------------------| +| clientId | `string` | Client ID for the user-assigned managed identity. | +| tenantId | `string` | Tenant ID where the managed identity is located. | +| scope | `string` | Resource identifier (application ID URI) with .default suffix. | + ### `_4.AssetAction` Management action configuration for assets. @@ -1501,40 +1584,29 @@ Common settings for the components. ## Outputs -| Name | Type | Description | -|:-----------------------------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------------| -| arcConnectedClusterName | `string` | The name of the Arc-enabled Kubernetes cluster that was connected to Azure. This can be used to reference the cluster in other deployments. | -| vmUsername | `string` | The administrative username that can be used to SSH into the deployed virtual machines. | -| vmNames | `array` | An array containing the names of all virtual machines that were deployed as part of this blueprint. | -| aksName | `string` | The AKS cluster name. | -| acrName | `string` | The Azure Container Registry name. | -| aiFoundryName | `string` | The AI Foundry account name. | -| aiFoundryEndpoint | `string` | The AI Foundry account endpoint. | -| aiFoundryPrincipalId | `string` | The AI Foundry account principal ID. | -| aioCertManagerExtensionId | `string` | The ID of the Azure IoT Operations Cert-Manager Extension. | -| aioCertManagerExtensionName | `string` | The name of the Azure IoT Operations Cert-Manager Extension. | -| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | -| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | -| customLocationId | `string` | The ID of the deployed Custom Location. | -| customLocationName | `string` | The name of the deployed Custom Location. | -| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | -| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | -| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | -| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | -| natGatewayId | `string` | The NAT Gateway ID (if enabled). | -| natGatewayName | `string` | The NAT Gateway name (if enabled). | -| defaultOutboundAccessEnabled | `bool` | Whether default outbound access is enabled (inverse of managed outbound). | -| privateResolverId | `string` | The Private DNS Resolver ID (if enabled). | -| privateResolverName | `string` | The Private DNS Resolver name (if enabled). | -| dnsServerIp | `string` | The DNS server IP from Private Resolver (if enabled). | -| privateEndpointsEnabled | `bool` | Whether private endpoints are enabled. | -| keyVaultPrivateEndpointId | `string` | The Key Vault private endpoint ID (if enabled). | -| storageBlobPrivateEndpointId | `string` | The storage account blob private endpoint ID (if enabled). | -| vpnGatewayId | `string` | The VPN Gateway ID (if enabled). | -| vpnGatewayName | `string` | The VPN Gateway name (if enabled). | -| vpnGatewayPublicIp | `string` | The VPN Gateway public IP address (if enabled). | -| vpnClientConnectionInfo | `object` | VPN client connection information (if enabled). | +| Name | Type | Description | +|:------------------------|:---------|:-------------------------------------------------------------------| +| azureIotOperations | `object` | Azure IoT Operations deployment details. | +| assets | `object` | IoT asset resources. | +| clusterConnection | `object` | Commands and information to connect to the deployed cluster. | +| containerRegistry | `object` | Azure Container Registry resources. | +| acrNetworkPosture | `object` | Azure Container Registry network posture metadata. | +| kubernetes | `object` | Azure Kubernetes Service resources. | +| dataStorage | `object` | Data storage resources. | +| deploymentSummary | `object` | Summary of the deployment configuration. | +| natGateway | `object` | NAT gateway resource when managed outbound access is enabled. | +| natGatewayPublicIps | `array` | Public IP resources associated with the NAT gateway keyed by name. | +| messaging | `object` | Cloud messaging resources. | +| aiFoundry | `object` | Azure AI Foundry account resources. | +| aiFoundryProjects | `array` | Azure AI Foundry project resources. | +| aiFoundryDeployments | `array` | Azure AI Foundry model deployments. | +| vmHost | `array` | Virtual machine host resources. | +| arcConnectedCluster | `object` | Azure Arc connected cluster resources. | +| observability | `object` | Monitoring and observability resources. | +| securityIdentity | `object` | Security and identity resources. | +| vpnGateway | `object` | VPN Gateway configuration when enabled. | +| vpnGatewayPublicIp | `string` | VPN Gateway public IP address for client configuration. | +| vpnClientConnectionInfo | `object` | VPN client connection information including download URLs. | +| privateResolverDnsIp | `string` | Private Resolver DNS IP address for VPN client configuration. | \ No newline at end of file diff --git a/blueprints/full-single-node-cluster/bicep/main.bicep b/blueprints/full-single-node-cluster/bicep/main.bicep index 0c935724..6e430e4c 100644 --- a/blueprints/full-single-node-cluster/bicep/main.bicep +++ b/blueprints/full-single-node-cluster/bicep/main.bicep @@ -226,6 +226,32 @@ param shouldEnableAkriSseConnector bool = false @description('List of custom Akri connector templates with user-defined endpoint types and container images.') param customAkriConnectors types.AkriConnectorTemplate[] = [] +@description('List of additional container registry endpoints. MCR is always added automatically.') +param registryEndpoints types.RegistryEndpointConfig[] = [] + +@description('Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication.') +param shouldIncludeAcrRegistryEndpoint bool = false + +/* + Local Variables +*/ + +var acrRegistryEndpoint = shouldIncludeAcrRegistryEndpoint + ? [ + { + name: 'acr-${common.resourcePrefix}' + host: '${cloudAcr.outputs.acrName}.azurecr.io' + acrResourceId: cloudAcr.outputs.acrId + authentication: { + method: 'SystemAssignedManagedIdentity' + systemAssignedManagedIdentitySettings: {} + } + } + ] + : [] + +var combinedRegistryEndpoints = concat(registryEndpoints, acrRegistryEndpoint) + /* Resources */ @@ -475,6 +501,9 @@ module edgeIotOps '../../../src/100-edge/110-iot-ops/bicep/main.bicep' = { shouldEnableAkriOnvifConnector: shouldEnableAkriOnvifConnector shouldEnableAkriSseConnector: shouldEnableAkriSseConnector customAkriConnectors: customAkriConnectors + + // Registry Endpoints Parameters + registryEndpoints: combinedRegistryEndpoints } } @@ -532,121 +561,187 @@ module edgeMessaging '../../../src/100-edge/130-messaging/bicep/main.bicep' = { Outputs */ -@description('The name of the Arc-enabled Kubernetes cluster that was connected to Azure. This can be used to reference the cluster in other deployments.') -output arcConnectedClusterName string = edgeCncfCluster.outputs.connectedClusterName - -@description('The administrative username that can be used to SSH into the deployed virtual machines.') -output vmUsername string = cloudVmHost.outputs.adminUsername - -@description('An array containing the names of all virtual machines that were deployed as part of this blueprint.') -output vmNames array = cloudVmHost.outputs.vmNames - -@description('The AKS cluster name.') -output aksName string? = cloudKubernetes.outputs.?aksName +/* + Azure IoT Operations Outputs +*/ +@description('Azure IoT Operations deployment details.') +output azureIotOperations object = { + customLocationId: edgeIotOps.outputs.customLocationId + instanceName: edgeIotOps.outputs.aioInstanceName + namespace: edgeIotOps.outputs.aioNamespace +} -@description('The Azure Container Registry name.') -output acrName string = cloudAcr.outputs.acrName +@description('IoT asset resources.') +output assets object = { + assets: edgeAssets.outputs.namespacedAssets + assetEndpointProfiles: edgeAssets.outputs.assetEndpointProfiles +} -@description('The AI Foundry account name.') -output aiFoundryName string? = shouldDeployAiFoundry ? cloudAiFoundry.?outputs.?aiFoundryName : null +/* + Cluster Connection Outputs +*/ -@description('The AI Foundry account endpoint.') -output aiFoundryEndpoint string? = shouldDeployAiFoundry ? cloudAiFoundry.?outputs.?aiFoundryEndpoint : null +@description('Commands and information to connect to the deployed cluster.') +output clusterConnection object = { + arcClusterName: edgeCncfCluster.outputs.connectedClusterName + arcClusterResourceGroup: cloudResourceGroup.outputs.resourceGroupName + arcProxyCommand: edgeCncfCluster.outputs.azureArcProxyCommand +} -@description('The AI Foundry account principal ID.') -output aiFoundryPrincipalId string? = shouldDeployAiFoundry ? cloudAiFoundry.?outputs.?aiFoundryPrincipalId : null +/* + Container Registry Outputs +*/ -@description('The ID of the Azure IoT Operations Cert-Manager Extension.') -output aioCertManagerExtensionId string = edgeArcExtensions.outputs.certManagerExtensionId +@description('Azure Container Registry resources.') +output containerRegistry object = { + id: cloudAcr.outputs.acrId + name: cloudAcr.outputs.acrName +} -@description('The name of the Azure IoT Operations Cert-Manager Extension.') -output aioCertManagerExtensionName string = edgeArcExtensions.outputs.certManagerExtensionName +@description('Azure Container Registry network posture metadata.') +output acrNetworkPosture object = { + isNatGatewayEnabled: cloudAcr.outputs.isNatGatewayEnabled +} -@description('The ID of the Secret Store Extension.') -output secretStoreExtensionId string = edgeIotOps.outputs.secretStoreExtensionId +@description('Azure Kubernetes Service resources.') +output kubernetes object? = shouldCreateAks + ? { + id: cloudKubernetes.outputs.?aksId + name: cloudKubernetes.outputs.?aksName + principalId: cloudKubernetes.outputs.?aksPrincipalId + } + : null -@description('The name of the Secret Store Extension.') -output secretStoreExtensionName string = edgeIotOps.outputs.secretStoreExtensionName +/* + Data Storage Outputs +*/ -@description('The ID of the deployed Custom Location.') -output customLocationId string = edgeIotOps.outputs.customLocationId +@description('Data storage resources.') +output dataStorage object = { + schemaRegistryName: cloudData.outputs.schemaRegistryName + storageAccountName: cloudData.outputs.storageAccountName +} -@description('The name of the deployed Custom Location.') -output customLocationName string = edgeIotOps.outputs.customLocationName +/* + Deployment Summary Outputs +*/ -@description('The ID of the deployed Azure IoT Operations instance.') -output aioInstanceId string = edgeIotOps.outputs.aioInstanceId +@description('Summary of the deployment configuration.') +output deploymentSummary object = { + resourceGroup: cloudResourceGroup.outputs.resourceGroupName +} -@description('The name of the deployed Azure IoT Operations instance.') -output aioInstanceName string = edgeIotOps.outputs.aioInstanceName +/* + Networking Outputs +*/ -@description('The ID of the deployed Azure IoT Operations Data Flow Profile.') -output dataFlowProfileId string = edgeIotOps.outputs.dataFlowProfileId +@description('NAT gateway resource when managed outbound access is enabled.') +output natGateway object? = shouldEnableManagedOutboundAccess + ? { + id: cloudNetworking.outputs.?natGatewayId + name: cloudNetworking.outputs.?natGatewayName + } + : null -@description('The name of the deployed Azure IoT Operations Data Flow Profile.') -output dataFlowProfileName string = edgeIotOps.outputs.dataFlowProfileName +@description('Public IP resources associated with the NAT gateway keyed by name.') +output natGatewayPublicIps array? = shouldEnableManagedOutboundAccess && cloudNetworking.outputs.?natGatewayPublicIps != null + ? cloudNetworking.outputs.?natGatewayPublicIps + : null -@description('The ID of the deployed Azure IoT Operations Data Flow Endpoint.') -output dataFlowEndpointId string = edgeIotOps.outputs.dataFlowEndpointId +/* + Messaging Outputs +*/ -@description('The name of the deployed Azure IoT Operations Data Flow Endpoint.') -output dataFlowEndpointName string = edgeIotOps.outputs.dataFlowEndpointName +@description('Cloud messaging resources.') +output messaging object = { + eventGridTopicEndpoint: cloudMessaging.outputs.eventGridMqttEndpoint != '' + ? cloudMessaging.outputs.eventGridMqttEndpoint + : 'Not deployed' + eventGridTopicName: cloudMessaging.outputs.eventGridTopicNames != '' + ? cloudMessaging.outputs.eventGridTopicNames + : 'Not deployed' + eventhubName: length(cloudMessaging.outputs.eventHubNames) > 0 + ? cloudMessaging.outputs.eventHubNames[0] + : 'Not deployed' + eventhubNamespaceName: cloudMessaging.outputs.eventHubNamespaceName != '' + ? cloudMessaging.outputs.eventHubNamespaceName + : 'Not deployed' +} /* - NAT Gateway Outputs + AI Foundry Outputs */ -@description('The NAT Gateway ID (if enabled).') -output natGatewayId string? = shouldEnableManagedOutboundAccess ? cloudNetworking.outputs.?natGatewayId : null +@description('Azure AI Foundry account resources.') +output aiFoundry object? = shouldDeployAiFoundry ? cloudAiFoundry.?outputs.?aiFoundry : null -@description('The NAT Gateway name (if enabled).') -output natGatewayName string? = shouldEnableManagedOutboundAccess ? cloudNetworking.outputs.?natGatewayName : null +@description('Azure AI Foundry project resources.') +output aiFoundryProjects array? = shouldDeployAiFoundry ? cloudAiFoundry.?outputs.?projectsArray : null -@description('Whether default outbound access is enabled (inverse of managed outbound).') -output defaultOutboundAccessEnabled bool = !shouldEnableManagedOutboundAccess +@description('Azure AI Foundry model deployments.') +output aiFoundryDeployments array? = shouldDeployAiFoundry ? cloudAiFoundry.?outputs.?deploymentsArray : null /* - Private Resolver Outputs + Edge Infrastructure Outputs */ -@description('The Private DNS Resolver ID (if enabled).') -output privateResolverId string? = shouldEnablePrivateResolver ? cloudNetworking.outputs.?privateResolverId : null - -@description('The Private DNS Resolver name (if enabled).') -output privateResolverName string? = shouldEnablePrivateResolver ? cloudNetworking.outputs.?privateResolverName : null +@description('Virtual machine host resources.') +output vmHost array = [ + { + id: cloudVmHost.outputs.vmIds[0] + location: common.location + name: cloudVmHost.outputs.vmNames[0] + } +] -@description('The DNS server IP from Private Resolver (if enabled).') -output dnsServerIp string? = shouldEnablePrivateResolver ? cloudNetworking.outputs.?dnsServerIp : null +@description('Azure Arc connected cluster resources.') +output arcConnectedCluster object = { + name: edgeCncfCluster.outputs.connectedClusterName + location: common.location +} /* - Private Endpoint Outputs + Observability Outputs */ -@description('Whether private endpoints are enabled.') -output privateEndpointsEnabled bool = shouldEnablePrivateEndpoints +@description('Monitoring and observability resources.') +output observability object = { + azureMonitorWorkspaceName: cloudObservability.outputs.monitorWorkspaceName + grafanaEndpoint: cloudObservability.outputs.grafanaEndpoint + grafanaName: cloudObservability.outputs.grafanaName + logAnalyticsWorkspaceName: cloudObservability.outputs.logAnalyticsName +} -@description('The Key Vault private endpoint ID (if enabled).') -output keyVaultPrivateEndpointId string? = shouldEnablePrivateEndpoints - ? cloudSecurityIdentity.outputs.?keyVaultPrivateEndpointId - : null +/* + Security and Identity Outputs +*/ -@description('The storage account blob private endpoint ID (if enabled).') -output storageBlobPrivateEndpointId string? = shouldEnablePrivateEndpoints - ? cloudData.outputs.?storageBlobPrivateEndpointId - : null +@description('Security and identity resources.') +output securityIdentity object = { + aioIdentity: cloudSecurityIdentity.outputs.aioIdentityName + keyVaultName: cloudSecurityIdentity.outputs.?keyVaultName ?? 'Not deployed' + keyVaultUri: cloudSecurityIdentity.outputs.?keyVaultName != null + ? 'https://${cloudSecurityIdentity.outputs.?keyVaultName}${environment().suffixes.keyvaultDns}/' + : 'Not deployed' +} /* VPN Gateway Outputs */ -@description('The VPN Gateway ID (if enabled).') -output vpnGatewayId string? = shouldEnableVpnGateway ? cloudVpnGateway.?outputs.?vpnGatewayId : null - -@description('The VPN Gateway name (if enabled).') -output vpnGatewayName string? = shouldEnableVpnGateway ? cloudVpnGateway.?outputs.?vpnGatewayName : null +@description('VPN Gateway configuration when enabled.') +output vpnGateway object? = shouldEnableVpnGateway + ? { + id: cloudVpnGateway.?outputs.?vpnGatewayId + name: cloudVpnGateway.?outputs.?vpnGatewayName + } + : null -@description('The VPN Gateway public IP address (if enabled).') +@description('VPN Gateway public IP address for client configuration.') output vpnGatewayPublicIp string? = shouldEnableVpnGateway ? cloudVpnGateway.?outputs.?vpnGatewayPublicIp : null -@description('VPN client connection information (if enabled).') +@description('VPN client connection information including download URLs.') output vpnClientConnectionInfo object? = shouldEnableVpnGateway ? cloudVpnGateway.?outputs.?clientConnectionInfo : null + +@description('Private Resolver DNS IP address for VPN client configuration.') +output privateResolverDnsIp string? = cloudNetworking.outputs.?dnsServerIp diff --git a/blueprints/full-single-node-cluster/terraform/README.md b/blueprints/full-single-node-cluster/terraform/README.md index 22db4563..739e8c8e 100644 --- a/blueprints/full-single-node-cluster/terraform/README.md +++ b/blueprints/full-single-node-cluster/terraform/README.md @@ -41,100 +41,102 @@ for a single-node cluster deployment, including observability, messaging, and da ## Inputs -| Name | Description | Type | Default | Required | -|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|:--------:| -| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes | -| location | Location for all resources in this module | `string` | n/a | yes | -| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes | -| acr\_allow\_trusted\_services | Whether trusted Azure services can bypass ACR network rules | `bool` | `true` | no | -| acr\_allowed\_public\_ip\_ranges | CIDR ranges permitted to reach the ACR public endpoint | `list(string)` | `[]` | no | -| acr\_data\_endpoint\_enabled | Whether to enable the dedicated ACR data endpoint | `bool` | `true` | no | -| acr\_public\_network\_access\_enabled | Whether to enable the ACR public endpoint alongside private connectivity | `bool` | `false` | no | -| acr\_sku | SKU name for the Azure Container Registry | `string` | `"Premium"` | no | -| ai\_foundry\_model\_deployments | Map of model deployments for AI Foundry | ```map(object({ name = string model = object({ format = string name = string version = string }) scale = object({ type = string capacity = number }) rai_policy_name = optional(string) version_upgrade_option = optional(string, "OnceNewDefaultVersionAvailable") }))``` | `{}` | no | -| ai\_foundry\_private\_dns\_zone\_ids | List of private DNS zone IDs for the AI Foundry private endpoint | `list(string)` | `[]` | no | -| ai\_foundry\_projects | Map of AI Foundry projects to create. SKU defaults to 'S0' (currently the only supported value) | ```map(object({ name = string display_name = string description = string sku = optional(string, "S0") }))``` | `{}` | no | -| ai\_foundry\_rai\_policies | Map of Responsible AI (RAI) content filtering policies. Must be created before referenced in model deployments. | ```map(object({ name = string base_policy_name = optional(string, "Microsoft.Default") mode = optional(string, "Blocking") content_filters = optional(list(object({ name = string enabled = optional(bool, true) blocking = optional(bool, true) severity_threshold = optional(string, "Medium") source = string })), []) }))``` | `{}` | no | -| ai\_foundry\_should\_enable\_local\_auth | Whether to enable local (API key) authentication for AI Foundry | `bool` | `true` | no | -| ai\_foundry\_should\_enable\_private\_endpoint | Whether to enable private endpoint for AI Foundry | `bool` | `false` | no | -| ai\_foundry\_should\_enable\_public\_network\_access | Whether to enable public network access to AI Foundry | `bool` | `true` | no | -| ai\_foundry\_sku | SKU name for the AI Foundry account | `string` | `"S0"` | no | -| aio\_features | AIO instance features with mode ('Stable', 'Preview', 'Disabled') and settings ('Enabled', 'Disabled') | ```map(object({ mode = optional(string) settings = optional(map(string)) }))``` | `null` | no | -| aks\_should\_enable\_private\_cluster | Whether to enable private cluster mode for AKS | `bool` | `true` | no | -| aks\_should\_enable\_private\_cluster\_public\_fqdn | Whether to create a private cluster public FQDN for AKS | `bool` | `false` | no | -| azureml\_ml\_workload\_subjects | Custom Kubernetes service account subjects for AzureML workload federation. Example: ['system:serviceaccount:azureml:azureml-workload', 'system:serviceaccount:osmo:osmo-workload'] | `list(string)` | `null` | no | -| azureml\_registry\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning registry when deployed | `bool` | `true` | no | -| azureml\_should\_create\_compute\_cluster | Whether to create a compute cluster for Azure Machine Learning training workloads | `bool` | `true` | no | -| azureml\_should\_create\_ml\_workload\_identity | Whether to create a user-assigned managed identity for AzureML workload federation. | `bool` | `false` | no | -| azureml\_should\_deploy\_registry | Whether to deploy Azure Machine Learning registry resources alongside the workspace | `bool` | `false` | no | -| azureml\_should\_enable\_private\_endpoint | Whether to enable a private endpoint for the Azure Machine Learning workspace | `bool` | `false` | no | -| azureml\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning workspace | `bool` | `true` | no | -| certificate\_subject | Certificate subject information for auto-generated certificates | ```object({ common_name = optional(string, "Full Single Node VPN Gateway Root Certificate") organization = optional(string, "Edge AI Accelerator") organizational_unit = optional(string, "IT") country = optional(string, "US") province = optional(string, "WA") locality = optional(string, "Redmond") })``` | `{}` | no | -| certificate\_validity\_days | Validity period in days for auto-generated certificates | `number` | `365` | no | -| custom\_akri\_connectors | List of custom Akri connector templates with user-defined endpoint types and container images. Supports built-in types (rest, media, onvif, sse) or custom types with custom\_endpoint\_type and custom\_image\_name. Built-in connectors default to mcr.microsoft.com/azureiotoperations/akri-connectors/connector\_type:0.5.1. | ```list(object({ name = string type = string // "rest", "media", "onvif", "sse", "custom" // Custom Connector Fields (required when type = "custom") custom_endpoint_type = optional(string) // e.g., "Contoso.Modbus", "Acme.CustomProtocol" custom_image_name = optional(string) // e.g., "my_acr.azurecr.io/custom-connector" custom_endpoint_version = optional(string, "1.0") // Runtime Configuration (defaults applied based on connector type) registry = optional(string) // Defaults: mcr.microsoft.com for built-in types image_tag = optional(string) // Defaults: 0.5.1 for built-in types, latest for custom replicas = optional(number, 1) image_pull_policy = optional(string) // Default: IfNotPresent // Diagnostics log_level = optional(string) // Default: info (lowercase: trace, debug, info, warning, error, critical) // MQTT Override (uses shared config if not provided) mqtt_config = optional(object({ host = string audience = string ca_configmap = string keep_alive_seconds = optional(number, 60) max_inflight_messages = optional(number, 100) session_expiry_seconds = optional(number, 600) })) // Optional Advanced Fields aio_min_version = optional(string) aio_max_version = optional(string) allocation = optional(object({ policy = string // "Bucketized" bucket_size = number // 1-100 })) additional_configuration = optional(map(string)) secrets = optional(list(object({ secret_alias = string secret_key = string secret_ref = string }))) trust_settings = optional(object({ trust_list_secret_ref = string })) }))``` | `[]` | no | -| custom\_locations\_oid | The object id of the Custom Locations Entra ID application for your tenant If none is provided, the script attempts to retrieve this value which requires 'Application.Read.All' or 'Directory.Read.All' permissions ```sh az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv``` | `string` | `null` | no | -| existing\_certificate\_name | Name of the existing certificate in Key Vault when vpn\_gateway\_should\_generate\_ca is false | `string` | `null` | no | -| instance | Instance identifier for naming resources: 001, 002, etc | `string` | `"001"` | no | -| namespaced\_assets | List of namespaced assets with enhanced configuration support | ```list(object({ name = string display_name = optional(string) device_ref = optional(object({ device_name = string endpoint_name = string })) asset_endpoint_profile_ref = optional(string) default_datasets_configuration = optional(string) default_streams_configuration = optional(string) default_events_configuration = optional(string) description = optional(string) documentation_uri = optional(string) enabled = optional(bool, true) hardware_revision = optional(string) manufacturer = optional(string) manufacturer_uri = optional(string) model = optional(string) product_code = optional(string) serial_number = optional(string) software_revision = optional(string) attributes = optional(map(string), {}) datasets = optional(list(object({ name = string data_points = list(object({ data_point_configuration = optional(string) data_source = string name = string observability_mode = optional(string) rest_sampling_interval_ms = optional(number) rest_mqtt_topic = optional(string) rest_include_state_store = optional(bool) rest_state_store_key = optional(string) })) dataset_configuration = optional(string) data_source = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) type_ref = optional(string) })), []) streams = optional(list(object({ name = string stream_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })), []) event_groups = optional(list(object({ name = string data_source = optional(string) event_group_configuration = optional(string) type_ref = optional(string) default_destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) events = list(object({ name = string data_source = string event_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })) })), []) management_groups = optional(list(object({ name = string data_source = optional(string) management_group_configuration = optional(string) type_ref = optional(string) default_topic = optional(string) default_timeout_in_seconds = optional(number, 100) actions = list(object({ name = string action_type = string target_uri = string topic = optional(string) timeout_in_seconds = optional(number) action_configuration = optional(string) type_ref = optional(string) })) })), []) }))``` | `[]` | no | -| namespaced\_devices | List of namespaced devices to create; otherwise, an empty list | ```list(object({ name = string enabled = optional(bool, true) endpoints = object({ outbound = optional(object({ assigned = object({}) }), { assigned = {} }) inbound = map(object({ endpoint_type = string address = string version = optional(string, null) additionalConfiguration = optional(string) authentication = object({ method = string usernamePasswordCredentials = optional(object({ usernameSecretName = string passwordSecretName = string })) x509Credentials = optional(object({ certificateSecretName = string })) }) trustSettings = optional(object({ trustList = string })) })) }) }))``` | `[]` | no | -| nat\_gateway\_idle\_timeout\_minutes | Idle timeout in minutes for NAT gateway connections | `number` | `4` | no | -| nat\_gateway\_public\_ip\_count | Number of public IP addresses to associate with the NAT gateway (example: 2) | `number` | `1` | no | -| nat\_gateway\_zones | Availability zones for NAT gateway resources when zone redundancy is required (example: ['1','2']) | `list(string)` | `[]` | no | -| node\_count | Number of nodes for the agent pool in the AKS cluster | `number` | `1` | no | -| node\_pools | Additional node pools for the AKS cluster; map key is used as the node pool name | ```map(object({ node_count = number vm_size = string subnet_address_prefixes = list(string) pod_subnet_address_prefixes = list(string) node_taints = optional(list(string), []) enable_auto_scaling = optional(bool, false) min_count = optional(number, null) max_count = optional(number, null) }))``` | `{}` | no | -| node\_vm\_size | VM size for the agent pool in the AKS cluster | `string` | `"Standard_D8ds_v5"` | no | -| postgresql\_admin\_password | Administrator password for PostgreSQL server. (Otherwise, generated when postgresql\_should\_generate\_admin\_password is true). | `string` | `null` | no | -| postgresql\_admin\_username | Administrator username for PostgreSQL server | `string` | `"pgadmin"` | no | -| postgresql\_databases | Map of databases to create with collation and charset | ```map(object({ collation = string charset = string }))``` | `null` | no | -| postgresql\_delegated\_subnet\_id | Subnet ID with delegation to Microsoft.DBforPostgreSQL/flexibleServers | `string` | `null` | no | -| postgresql\_should\_enable\_extensions | Whether to enable PostgreSQL extensions via azure.extensions | `bool` | `true` | no | -| postgresql\_should\_enable\_geo\_redundant\_backup | Whether to enable geo-redundant backups for PostgreSQL | `bool` | `false` | no | -| postgresql\_should\_enable\_timescaledb | Whether to enable TimescaleDB extension for PostgreSQL | `bool` | `true` | no | -| postgresql\_should\_generate\_admin\_password | Whether to auto-generate PostgreSQL admin password. | `bool` | `true` | no | -| postgresql\_should\_store\_credentials\_in\_key\_vault | Whether to store PostgreSQL admin credentials in Key Vault. | `bool` | `true` | no | -| postgresql\_sku\_name | SKU name for PostgreSQL server | `string` | `"GP_Standard_D2s_v3"` | no | -| postgresql\_storage\_mb | Storage size in megabytes for PostgreSQL | `number` | `32768` | no | -| postgresql\_version | PostgreSQL server version | `string` | `"16"` | no | -| redis\_clustering\_policy | Clustering policy for Redis cache (OSSCluster or EnterpriseCluster) | `string` | `"OSSCluster"` | no | -| redis\_should\_enable\_high\_availability | Whether to enable high availability for Redis cache | `bool` | `true` | no | -| redis\_sku\_name | SKU name for Azure Managed Redis cache | `string` | `"Balanced_B10"` | no | -| resolver\_subnet\_address\_prefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets | `string` | `"10.0.9.0/28"` | no | -| resource\_group\_name | Name of the resource group to create or use. Otherwise, 'rg-{resource\_prefix}-{environment}-{instance}' | `string` | `null` | no | -| should\_add\_current\_user\_cluster\_admin | Whether to give the current signed-in user cluster-admin permissions on the new cluster | `bool` | `true` | no | -| should\_create\_aks | Whether to deploy Azure Kubernetes Service | `bool` | `false` | no | -| should\_create\_aks\_identity | Whether to create a user-assigned identity for the AKS cluster when using custom private DNS zones | `bool` | `false` | no | -| should\_create\_anonymous\_broker\_listener | Whether to enable an insecure anonymous AIO MQ broker listener; use only for dev or test environments | `bool` | `false` | no | -| should\_create\_azure\_functions | Whether to create the Azure Functions resources including the App Service plan | `bool` | `false` | no | -| should\_deploy\_ai\_foundry | Whether to deploy Azure AI Foundry resources | `bool` | `false` | no | -| should\_deploy\_azureml | Whether to deploy the Azure Machine Learning workspace and optional compute cluster | `bool` | `false` | no | -| should\_deploy\_edge\_azureml | Whether to deploy the Azure Machine Learning edge extension when Azure ML is enabled | `bool` | `false` | no | -| should\_deploy\_postgresql | Whether to deploy PostgreSQL Flexible Server component | `bool` | `false` | no | -| should\_deploy\_redis | Whether to deploy Azure Managed Redis component | `bool` | `false` | no | -| should\_deploy\_resource\_sync\_rules | Whether to deploy resource sync rules | `bool` | `true` | no | -| should\_enable\_akri\_media\_connector | Whether to deploy the Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_akri\_onvif\_connector | Whether to deploy the Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_akri\_rest\_connector | Whether to deploy the Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_akri\_sse\_connector | Whether to deploy the Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | -| should\_enable\_key\_vault\_public\_network\_access | Whether to enable public network access for the Key Vault | `bool` | `true` | no | -| should\_enable\_managed\_outbound\_access | Whether to enable managed outbound egress via NAT gateway instead of platform default internet access | `bool` | `true` | no | -| should\_enable\_oidc\_issuer | Whether to enable the OIDC issuer URL for the cluster | `bool` | `true` | no | -| should\_enable\_opc\_ua\_simulator | Whether to deploy the OPC UA simulator to the cluster | `bool` | `false` | no | -| should\_enable\_private\_endpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources to route monitoring ingestion through private link | `bool` | `false` | no | -| should\_enable\_private\_resolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints | `bool` | `false` | no | -| should\_enable\_storage\_public\_network\_access | Whether to enable public network access for the storage account | `bool` | `true` | no | -| should\_enable\_vpn\_gateway | Whether to create a VPN gateway for secure access to private endpoints | `bool` | `false` | no | -| should\_enable\_workload\_identity | Whether to enable Azure AD workload identity for the cluster | `bool` | `true` | no | -| should\_get\_custom\_locations\_oid | Whether to get the Custom Locations object ID using Terraform's azuread provider Otherwise, provide 'custom\_locations\_oid' or rely on `az connectedk8s enable-features` during cluster setup | `bool` | `true` | no | -| storage\_account\_is\_hns\_enabled | Whether to enable hierarchical namespace on the storage account when Azure Machine Learning is not deployed; automatically forced to false when should\_deploy\_azureml is true | `bool` | `true` | no | -| tags | Tags to apply to all resources in this blueprint | `map(string)` | `{}` | no | -| use\_existing\_resource\_group | Whether to use an existing resource group with the provided or computed name instead of creating a new one | `bool` | `false` | no | -| vpn\_gateway\_config | VPN gateway configuration including SKU, generation, client address pool, and supported protocols | ```object({ sku = optional(string, "VpnGw1") generation = optional(string, "Generation1") client_address_pool = optional(list(string), ["192.168.200.0/24"]) protocols = optional(list(string), ["OpenVPN", "IkeV2"]) })``` | `{}` | no | -| vpn\_gateway\_should\_generate\_ca | Whether to generate a new CA certificate; when false, uses an existing certificate from Key Vault | `bool` | `true` | no | -| vpn\_gateway\_should\_use\_azure\_ad\_auth | Whether to use Azure AD authentication for the VPN gateway; otherwise, certificate authentication is used | `bool` | `true` | no | -| vpn\_gateway\_subnet\_address\_prefixes | Address prefixes for the GatewaySubnet; must be /27 or larger | `list(string)` | ```[ "10.0.2.0/27" ]``` | no | -| vpn\_site\_connections | Site-to-site VPN site definitions. Use non-overlapping on-premises address spaces and reference shared keys via shared\_key\_reference | ```list(object({ name = string address_spaces = list(string) shared_key_reference = string connection_mode = optional(string, "Default") dpd_timeout_seconds = optional(number) gateway_fqdn = optional(string) gateway_ip_address = optional(string) ike_protocol = optional(string, "IKEv2") use_policy_based_selectors = optional(bool, false) bgp_settings = optional(object({ asn = number peer_address = string peer_weight = optional(number) })) ipsec_policy = optional(object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })) }))``` | `[]` | no | -| vpn\_site\_default\_ipsec\_policy | Fallback IPsec policy applied when site definitions omit ipsec\_policy overrides | ```object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })``` | `null` | no | -| vpn\_site\_shared\_keys | Pre-shared keys for site definitions keyed by shared\_key\_reference. Source values from secure secret storage | `map(string)` | `{}` | no | +| Name | Description | Type | Default | Required | +|------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|:--------:| +| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes | +| location | Location for all resources in this module | `string` | n/a | yes | +| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes | +| acr\_allow\_trusted\_services | Whether trusted Azure services can bypass ACR network rules | `bool` | `true` | no | +| acr\_allowed\_public\_ip\_ranges | CIDR ranges permitted to reach the ACR public endpoint | `list(string)` | `[]` | no | +| acr\_data\_endpoint\_enabled | Whether to enable the dedicated ACR data endpoint | `bool` | `true` | no | +| acr\_public\_network\_access\_enabled | Whether to enable the ACR public endpoint alongside private connectivity | `bool` | `false` | no | +| acr\_sku | SKU name for the Azure Container Registry | `string` | `"Premium"` | no | +| ai\_foundry\_model\_deployments | Map of model deployments for AI Foundry | ```map(object({ name = string model = object({ format = string name = string version = string }) scale = object({ type = string capacity = number }) rai_policy_name = optional(string) version_upgrade_option = optional(string, "OnceNewDefaultVersionAvailable") }))``` | `{}` | no | +| ai\_foundry\_private\_dns\_zone\_ids | List of private DNS zone IDs for the AI Foundry private endpoint | `list(string)` | `[]` | no | +| ai\_foundry\_projects | Map of AI Foundry projects to create. SKU defaults to 'S0' (currently the only supported value) | ```map(object({ name = string display_name = string description = string sku = optional(string, "S0") }))``` | `{}` | no | +| ai\_foundry\_rai\_policies | Map of Responsible AI (RAI) content filtering policies. Must be created before referenced in model deployments. | ```map(object({ name = string base_policy_name = optional(string, "Microsoft.Default") mode = optional(string, "Blocking") content_filters = optional(list(object({ name = string enabled = optional(bool, true) blocking = optional(bool, true) severity_threshold = optional(string, "Medium") source = string })), []) }))``` | `{}` | no | +| ai\_foundry\_should\_enable\_local\_auth | Whether to enable local (API key) authentication for AI Foundry | `bool` | `true` | no | +| ai\_foundry\_should\_enable\_private\_endpoint | Whether to enable private endpoint for AI Foundry | `bool` | `false` | no | +| ai\_foundry\_should\_enable\_public\_network\_access | Whether to enable public network access to AI Foundry | `bool` | `true` | no | +| ai\_foundry\_sku | SKU name for the AI Foundry account | `string` | `"S0"` | no | +| aio\_features | AIO instance features with mode ('Stable', 'Preview', 'Disabled') and settings ('Enabled', 'Disabled') | ```map(object({ mode = optional(string) settings = optional(map(string)) }))``` | `null` | no | +| aks\_should\_enable\_private\_cluster | Whether to enable private cluster mode for AKS | `bool` | `true` | no | +| aks\_should\_enable\_private\_cluster\_public\_fqdn | Whether to create a private cluster public FQDN for AKS | `bool` | `false` | no | +| azureml\_ml\_workload\_subjects | Custom Kubernetes service account subjects for AzureML workload federation. Example: ['system:serviceaccount:azureml:azureml-workload', 'system:serviceaccount:osmo:osmo-workload'] | `list(string)` | `null` | no | +| azureml\_registry\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning registry when deployed | `bool` | `true` | no | +| azureml\_should\_create\_compute\_cluster | Whether to create a compute cluster for Azure Machine Learning training workloads | `bool` | `true` | no | +| azureml\_should\_create\_ml\_workload\_identity | Whether to create a user-assigned managed identity for AzureML workload federation. | `bool` | `false` | no | +| azureml\_should\_deploy\_registry | Whether to deploy Azure Machine Learning registry resources alongside the workspace | `bool` | `false` | no | +| azureml\_should\_enable\_private\_endpoint | Whether to enable a private endpoint for the Azure Machine Learning workspace | `bool` | `false` | no | +| azureml\_should\_enable\_public\_network\_access | Whether to enable public network access to the Azure Machine Learning workspace | `bool` | `true` | no | +| certificate\_subject | Certificate subject information for auto-generated certificates | ```object({ common_name = optional(string, "Full Single Node VPN Gateway Root Certificate") organization = optional(string, "Edge AI Accelerator") organizational_unit = optional(string, "IT") country = optional(string, "US") province = optional(string, "WA") locality = optional(string, "Redmond") })``` | `{}` | no | +| certificate\_validity\_days | Validity period in days for auto-generated certificates | `number` | `365` | no | +| custom\_akri\_connectors | List of custom Akri connector templates with user-defined endpoint types and container images. Supports built-in types (rest, media, onvif, sse) or custom types with custom\_endpoint\_type and custom\_image\_name. Built-in connectors default to mcr.microsoft.com/azureiotoperations/akri-connectors/connector\_type:0.5.1. | ```list(object({ name = string type = string // "rest", "media", "onvif", "sse", "custom" // Custom Connector Fields (required when type = "custom") custom_endpoint_type = optional(string) // e.g., "Contoso.Modbus", "Acme.CustomProtocol" custom_image_name = optional(string) // e.g., "my_acr.azurecr.io/custom-connector" custom_endpoint_version = optional(string, "1.0") // Runtime Configuration (defaults applied based on connector type) registry = optional(string) // Defaults: mcr.microsoft.com for built-in types image_tag = optional(string) // Defaults: 0.5.1 for built-in types, latest for custom replicas = optional(number, 1) image_pull_policy = optional(string) // Default: IfNotPresent // Diagnostics log_level = optional(string) // Default: info (lowercase: trace, debug, info, warning, error, critical) // MQTT Override (uses shared config if not provided) mqtt_config = optional(object({ host = string audience = string ca_configmap = string keep_alive_seconds = optional(number, 60) max_inflight_messages = optional(number, 100) session_expiry_seconds = optional(number, 600) })) // Optional Advanced Fields aio_min_version = optional(string) aio_max_version = optional(string) allocation = optional(object({ policy = string // "Bucketized" bucket_size = number // 1-100 })) additional_configuration = optional(map(string)) secrets = optional(list(object({ secret_alias = string secret_key = string secret_ref = string }))) trust_settings = optional(object({ trust_list_secret_ref = string })) }))``` | `[]` | no | +| custom\_locations\_oid | The object id of the Custom Locations Entra ID application for your tenant If none is provided, the script attempts to retrieve this value which requires 'Application.Read.All' or 'Directory.Read.All' permissions ```sh az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv``` | `string` | `null` | no | +| existing\_certificate\_name | Name of the existing certificate in Key Vault when vpn\_gateway\_should\_generate\_ca is false | `string` | `null` | no | +| instance | Instance identifier for naming resources: 001, 002, etc | `string` | `"001"` | no | +| namespaced\_assets | List of namespaced assets with enhanced configuration support | ```list(object({ name = string display_name = optional(string) device_ref = optional(object({ device_name = string endpoint_name = string })) asset_endpoint_profile_ref = optional(string) default_datasets_configuration = optional(string) default_streams_configuration = optional(string) default_events_configuration = optional(string) description = optional(string) documentation_uri = optional(string) enabled = optional(bool, true) hardware_revision = optional(string) manufacturer = optional(string) manufacturer_uri = optional(string) model = optional(string) product_code = optional(string) serial_number = optional(string) software_revision = optional(string) attributes = optional(map(string), {}) datasets = optional(list(object({ name = string data_points = list(object({ data_point_configuration = optional(string) data_source = string name = string observability_mode = optional(string) rest_sampling_interval_ms = optional(number) rest_mqtt_topic = optional(string) rest_include_state_store = optional(bool) rest_state_store_key = optional(string) })) dataset_configuration = optional(string) data_source = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) type_ref = optional(string) })), []) streams = optional(list(object({ name = string stream_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })), []) event_groups = optional(list(object({ name = string data_source = optional(string) event_group_configuration = optional(string) type_ref = optional(string) default_destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) events = list(object({ name = string data_source = string event_configuration = optional(string) type_ref = optional(string) destinations = optional(list(object({ target = string configuration = object({ topic = optional(string) retain = optional(string) qos = optional(string) }) })), []) })) })), []) management_groups = optional(list(object({ name = string data_source = optional(string) management_group_configuration = optional(string) type_ref = optional(string) default_topic = optional(string) default_timeout_in_seconds = optional(number, 100) actions = list(object({ name = string action_type = string target_uri = string topic = optional(string) timeout_in_seconds = optional(number) action_configuration = optional(string) type_ref = optional(string) })) })), []) }))``` | `[]` | no | +| namespaced\_devices | List of namespaced devices to create; otherwise, an empty list | ```list(object({ name = string enabled = optional(bool, true) endpoints = object({ outbound = optional(object({ assigned = object({}) }), { assigned = {} }) inbound = map(object({ endpoint_type = string address = string version = optional(string, null) additionalConfiguration = optional(string) authentication = object({ method = string usernamePasswordCredentials = optional(object({ usernameSecretName = string passwordSecretName = string })) x509Credentials = optional(object({ certificateSecretName = string })) }) trustSettings = optional(object({ trustList = string })) })) }) }))``` | `[]` | no | +| nat\_gateway\_idle\_timeout\_minutes | Idle timeout in minutes for NAT gateway connections | `number` | `4` | no | +| nat\_gateway\_public\_ip\_count | Number of public IP addresses to associate with the NAT gateway (example: 2) | `number` | `1` | no | +| nat\_gateway\_zones | Availability zones for NAT gateway resources when zone redundancy is required (example: ['1','2']) | `list(string)` | `[]` | no | +| node\_count | Number of nodes for the agent pool in the AKS cluster | `number` | `1` | no | +| node\_pools | Additional node pools for the AKS cluster; map key is used as the node pool name | ```map(object({ node_count = number vm_size = string subnet_address_prefixes = list(string) pod_subnet_address_prefixes = list(string) node_taints = optional(list(string), []) enable_auto_scaling = optional(bool, false) min_count = optional(number, null) max_count = optional(number, null) }))``` | `{}` | no | +| node\_vm\_size | VM size for the agent pool in the AKS cluster | `string` | `"Standard_D8ds_v5"` | no | +| postgresql\_admin\_password | Administrator password for PostgreSQL server. (Otherwise, generated when postgresql\_should\_generate\_admin\_password is true). | `string` | `null` | no | +| postgresql\_admin\_username | Administrator username for PostgreSQL server | `string` | `"pgadmin"` | no | +| postgresql\_databases | Map of databases to create with collation and charset | ```map(object({ collation = string charset = string }))``` | `null` | no | +| postgresql\_delegated\_subnet\_id | Subnet ID with delegation to Microsoft.DBforPostgreSQL/flexibleServers | `string` | `null` | no | +| postgresql\_should\_enable\_extensions | Whether to enable PostgreSQL extensions via azure.extensions | `bool` | `true` | no | +| postgresql\_should\_enable\_geo\_redundant\_backup | Whether to enable geo-redundant backups for PostgreSQL | `bool` | `false` | no | +| postgresql\_should\_enable\_timescaledb | Whether to enable TimescaleDB extension for PostgreSQL | `bool` | `true` | no | +| postgresql\_should\_generate\_admin\_password | Whether to auto-generate PostgreSQL admin password. | `bool` | `true` | no | +| postgresql\_should\_store\_credentials\_in\_key\_vault | Whether to store PostgreSQL admin credentials in Key Vault. | `bool` | `true` | no | +| postgresql\_sku\_name | SKU name for PostgreSQL server | `string` | `"GP_Standard_D2s_v3"` | no | +| postgresql\_storage\_mb | Storage size in megabytes for PostgreSQL | `number` | `32768` | no | +| postgresql\_version | PostgreSQL server version | `string` | `"16"` | no | +| redis\_clustering\_policy | Clustering policy for Redis cache (OSSCluster or EnterpriseCluster) | `string` | `"OSSCluster"` | no | +| redis\_should\_enable\_high\_availability | Whether to enable high availability for Redis cache | `bool` | `true` | no | +| redis\_sku\_name | SKU name for Azure Managed Redis cache | `string` | `"Balanced_B10"` | no | +| registry\_endpoints | List of additional container registry endpoints for pulling custom artifacts (WASM modules, graph definitions, connector templates). MCR (mcr.microsoft.com) is always added automatically with anonymous authentication. The `acr_resource_id` field enables automatic AcrPull role assignment for ACR endpoints using SystemAssignedManagedIdentity authentication. When `should_assign_acr_pull_for_aio` is true and `acr_resource_id` is provided, the AIO extension's identity will be granted AcrPull access to the specified ACR. | ```list(object({ name = string host = string acr_resource_id = optional(string) should_assign_acr_pull_for_aio = optional(bool, false) authentication = object({ method = string system_assigned_managed_identity_settings = optional(object({ audience = optional(string) })) user_assigned_managed_identity_settings = optional(object({ client_id = string tenant_id = string scope = optional(string) })) artifact_pull_secret_settings = optional(object({ secret_ref = string })) }) }))``` | `[]` | no | +| resolver\_subnet\_address\_prefix | Address prefix for the private resolver subnet; must be /28 or larger and not overlap with other subnets | `string` | `"10.0.9.0/28"` | no | +| resource\_group\_name | Name of the resource group to create or use. Otherwise, 'rg-{resource\_prefix}-{environment}-{instance}' | `string` | `null` | no | +| should\_add\_current\_user\_cluster\_admin | Whether to give the current signed-in user cluster-admin permissions on the new cluster | `bool` | `true` | no | +| should\_create\_aks | Whether to deploy Azure Kubernetes Service | `bool` | `false` | no | +| should\_create\_aks\_identity | Whether to create a user-assigned identity for the AKS cluster when using custom private DNS zones | `bool` | `false` | no | +| should\_create\_anonymous\_broker\_listener | Whether to enable an insecure anonymous AIO MQ broker listener; use only for dev or test environments | `bool` | `false` | no | +| should\_create\_azure\_functions | Whether to create the Azure Functions resources including the App Service plan | `bool` | `false` | no | +| should\_deploy\_ai\_foundry | Whether to deploy Azure AI Foundry resources | `bool` | `false` | no | +| should\_deploy\_azureml | Whether to deploy the Azure Machine Learning workspace and optional compute cluster | `bool` | `false` | no | +| should\_deploy\_edge\_azureml | Whether to deploy the Azure Machine Learning edge extension when Azure ML is enabled | `bool` | `false` | no | +| should\_deploy\_postgresql | Whether to deploy PostgreSQL Flexible Server component | `bool` | `false` | no | +| should\_deploy\_redis | Whether to deploy Azure Managed Redis component | `bool` | `false` | no | +| should\_deploy\_resource\_sync\_rules | Whether to deploy resource sync rules | `bool` | `true` | no | +| should\_enable\_akri\_media\_connector | Whether to deploy the Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_akri\_onvif\_connector | Whether to deploy the Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_akri\_rest\_connector | Whether to deploy the Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_akri\_sse\_connector | Whether to deploy the Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | +| should\_enable\_key\_vault\_public\_network\_access | Whether to enable public network access for the Key Vault | `bool` | `true` | no | +| should\_enable\_managed\_outbound\_access | Whether to enable managed outbound egress via NAT gateway instead of platform default internet access | `bool` | `true` | no | +| should\_enable\_oidc\_issuer | Whether to enable the OIDC issuer URL for the cluster | `bool` | `true` | no | +| should\_enable\_opc\_ua\_simulator | Whether to deploy the OPC UA simulator to the cluster | `bool` | `false` | no | +| should\_enable\_private\_endpoints | Whether to enable private endpoints across Key Vault, storage, and observability resources to route monitoring ingestion through private link | `bool` | `false` | no | +| should\_enable\_private\_resolver | Whether to enable Azure Private Resolver for VPN client DNS resolution of private endpoints | `bool` | `false` | no | +| should\_enable\_storage\_public\_network\_access | Whether to enable public network access for the storage account | `bool` | `true` | no | +| should\_enable\_vpn\_gateway | Whether to create a VPN gateway for secure access to private endpoints | `bool` | `false` | no | +| should\_enable\_workload\_identity | Whether to enable Azure AD workload identity for the cluster | `bool` | `true` | no | +| should\_get\_custom\_locations\_oid | Whether to get the Custom Locations object ID using Terraform's azuread provider Otherwise, provide 'custom\_locations\_oid' or rely on `az connectedk8s enable-features` during cluster setup | `bool` | `true` | no | +| should\_include\_acr\_registry\_endpoint | Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication | `bool` | `false` | no | +| storage\_account\_is\_hns\_enabled | Whether to enable hierarchical namespace on the storage account when Azure Machine Learning is not deployed; automatically forced to false when should\_deploy\_azureml is true | `bool` | `true` | no | +| tags | Tags to apply to all resources in this blueprint | `map(string)` | `{}` | no | +| use\_existing\_resource\_group | Whether to use an existing resource group with the provided or computed name instead of creating a new one | `bool` | `false` | no | +| vpn\_gateway\_config | VPN gateway configuration including SKU, generation, client address pool, and supported protocols | ```object({ sku = optional(string, "VpnGw1") generation = optional(string, "Generation1") client_address_pool = optional(list(string), ["192.168.200.0/24"]) protocols = optional(list(string), ["OpenVPN", "IkeV2"]) })``` | `{}` | no | +| vpn\_gateway\_should\_generate\_ca | Whether to generate a new CA certificate; when false, uses an existing certificate from Key Vault | `bool` | `true` | no | +| vpn\_gateway\_should\_use\_azure\_ad\_auth | Whether to use Azure AD authentication for the VPN gateway; otherwise, certificate authentication is used | `bool` | `true` | no | +| vpn\_gateway\_subnet\_address\_prefixes | Address prefixes for the GatewaySubnet; must be /27 or larger | `list(string)` | ```[ "10.0.2.0/27" ]``` | no | +| vpn\_site\_connections | Site-to-site VPN site definitions. Use non-overlapping on-premises address spaces and reference shared keys via shared\_key\_reference | ```list(object({ name = string address_spaces = list(string) shared_key_reference = string connection_mode = optional(string, "Default") dpd_timeout_seconds = optional(number) gateway_fqdn = optional(string) gateway_ip_address = optional(string) ike_protocol = optional(string, "IKEv2") use_policy_based_selectors = optional(bool, false) bgp_settings = optional(object({ asn = number peer_address = string peer_weight = optional(number) })) ipsec_policy = optional(object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })) }))``` | `[]` | no | +| vpn\_site\_default\_ipsec\_policy | Fallback IPsec policy applied when site definitions omit ipsec\_policy overrides | ```object({ dh_group = string ike_encryption = string ike_integrity = string ipsec_encryption = string ipsec_integrity = string pfs_group = string sa_datasize_kb = optional(number) sa_lifetime_seconds = optional(number) })``` | `null` | no | +| vpn\_site\_shared\_keys | Pre-shared keys for site definitions keyed by shared\_key\_reference. Source values from secure secret storage | `map(string)` | `{}` | no | ## Outputs diff --git a/blueprints/full-single-node-cluster/terraform/main.tf b/blueprints/full-single-node-cluster/terraform/main.tf index 1e9c412f..758e49bd 100644 --- a/blueprints/full-single-node-cluster/terraform/main.tf +++ b/blueprints/full-single-node-cluster/terraform/main.tf @@ -7,6 +7,21 @@ locals { default_outbound_access_enabled = var.should_enable_managed_outbound_access == false + + acr_registry_endpoint = var.should_include_acr_registry_endpoint ? [{ + name = "acr-${var.resource_prefix}" + host = "${module.cloud_acr.acr.name}.azurecr.io" + acr_resource_id = module.cloud_acr.acr.id + should_assign_acr_pull_for_aio = true + authentication = { + method = "SystemAssignedManagedIdentity" + system_assigned_managed_identity_settings = null + user_assigned_managed_identity_settings = null + artifact_pull_secret_settings = null + } + }] : [] + + combined_registry_endpoints = concat(var.registry_endpoints, local.acr_registry_endpoint) } module "cloud_resource_group" { @@ -402,6 +417,7 @@ module "edge_iot_ops" { should_enable_akri_onvif_connector = var.should_enable_akri_onvif_connector should_enable_akri_sse_connector = var.should_enable_akri_sse_connector custom_akri_connectors = var.custom_akri_connectors + registry_endpoints = local.combined_registry_endpoints } diff --git a/blueprints/full-single-node-cluster/terraform/outputs.tf b/blueprints/full-single-node-cluster/terraform/outputs.tf index 88057467..78c2b18e 100644 --- a/blueprints/full-single-node-cluster/terraform/outputs.tf +++ b/blueprints/full-single-node-cluster/terraform/outputs.tf @@ -138,8 +138,8 @@ output "messaging" { description = "Cloud messaging resources." value = { event_grid_topic_endpoint = try(module.cloud_messaging.eventgrid.endpoint, "Not deployed") - event_grid_topic_name = try(module.cloud_messaging.eventgrid.name, "Not deployed") - eventhub_name = try(module.cloud_messaging.eventhubs[0].name, "Not deployed") + event_grid_topic_name = try(module.cloud_messaging.eventgrid.topic_name, "Not deployed") + eventhub_name = try(module.cloud_messaging.eventhubs[0].eventhub_name, "Not deployed") eventhub_namespace_name = try(module.cloud_messaging.eventhubs[0].namespace_name, "Not deployed") } } diff --git a/blueprints/full-single-node-cluster/terraform/variables.tf b/blueprints/full-single-node-cluster/terraform/variables.tf index 22662de5..1e38095d 100644 --- a/blueprints/full-single-node-cluster/terraform/variables.tf +++ b/blueprints/full-single-node-cluster/terraform/variables.tf @@ -897,6 +897,101 @@ variable "custom_akri_connectors" { } } +variable "registry_endpoints" { + type = list(object({ + name = string + host = string + acr_resource_id = optional(string) + should_assign_acr_pull_for_aio = optional(bool, false) + + authentication = object({ + method = string + system_assigned_managed_identity_settings = optional(object({ + audience = optional(string) + })) + user_assigned_managed_identity_settings = optional(object({ + client_id = string + tenant_id = string + scope = optional(string) + })) + artifact_pull_secret_settings = optional(object({ + secret_ref = string + })) + }) + })) + + default = [] + description = <<-EOT + List of additional container registry endpoints for pulling custom artifacts (WASM modules, graph definitions, connector templates). + MCR (mcr.microsoft.com) is always added automatically with anonymous authentication. + + The `acr_resource_id` field enables automatic AcrPull role assignment for ACR endpoints + using SystemAssignedManagedIdentity authentication. When `should_assign_acr_pull_for_aio` is true + and `acr_resource_id` is provided, the AIO extension's identity will be granted AcrPull access to the specified ACR. + EOT + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + can(regex("^[a-z0-9][a-z0-9-]*[a-z0-9]$", ep.name)) && length(ep.name) >= 3 && length(ep.name) <= 63 + ]) + error_message = "Registry endpoint name must be 3-63 characters, contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + contains(["SystemAssignedManagedIdentity", "UserAssignedManagedIdentity", "ArtifactPullSecret", "Anonymous"], ep.authentication.method) + ]) + error_message = "Authentication method must be one of: SystemAssignedManagedIdentity, UserAssignedManagedIdentity, ArtifactPullSecret, Anonymous" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.authentication.method != "UserAssignedManagedIdentity" || ( + ep.authentication.user_assigned_managed_identity_settings != null && + ep.authentication.user_assigned_managed_identity_settings.client_id != null && + ep.authentication.user_assigned_managed_identity_settings.tenant_id != null + ) + ]) + error_message = "UserAssignedManagedIdentity authentication requires client_id and tenant_id in user_assigned_managed_identity_settings" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.authentication.method != "ArtifactPullSecret" || ( + ep.authentication.artifact_pull_secret_settings != null && + ep.authentication.artifact_pull_secret_settings.secret_ref != null + ) + ]) + error_message = "ArtifactPullSecret authentication requires secret_ref in artifact_pull_secret_settings" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.name != "mcr" && ep.name != "default" + ]) + error_message = "Registry endpoint names 'mcr' and 'default' are reserved" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.acr_resource_id == null || ep.authentication.method == "SystemAssignedManagedIdentity" + ]) + error_message = "acr_resource_id can only be specified with SystemAssignedManagedIdentity authentication method" + } +} + +variable "should_include_acr_registry_endpoint" { + type = bool + default = false + description = "Whether to include the deployed ACR as a registry endpoint with System Assigned Managed Identity authentication" +} + # ------------------------------------------------------------------------------ # AI Foundry Variables # ------------------------------------------------------------------------------ diff --git a/blueprints/full-single-node-cluster/tests/README.md b/blueprints/full-single-node-cluster/tests/README.md new file mode 100644 index 00000000..3e684868 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/README.md @@ -0,0 +1,495 @@ +# Tests for Full Single Node Cluster Blueprint + +This directory contains comprehensive integration and contract tests for the [Full Single Node Cluster](../README.md) blueprint supporting both Terraform and Bicep deployments. + +## Quick Start + +```bash +# Contract tests (fast, zero-cost validation) +./run-contract-tests.sh both + +# Terraform deployment tests (creates Azure resources) +./run-deployment-tests.sh terraform + +# Bicep deployment tests (creates Azure resources, auto-generates password) +./run-deployment-tests.sh bicep + +# Run both deployment frameworks +./run-deployment-tests.sh both +``` + +## Test Architecture + +The test suite provides two complementary testing strategies: + +### 1. Contract Tests (Fast, Zero-Cost) + +**Purpose:** Static validation of IaC output declarations without deployment + +- โœ… Validates `BlueprintOutputs` struct matches IaC output declarations +- โšก Executes in seconds without Azure authentication +- ๐Ÿ’ฐ Zero cost - no Azure resources created +- ๐Ÿ”ง Uses `terraform-docs` and `az bicep build` for static analysis +- ๐Ÿ› Catches configuration drift before expensive deployments + +### 2. Deployment Tests (Full Integration) + +**Purpose:** End-to-end deployment and functional validation + +- ๐Ÿš€ Deploys complete infrastructure to Azure +- โœ… Validates resource creation and configuration +- ๐Ÿ”Œ Tests Kubernetes cluster connectivity and operations +- โ˜๏ธ Verifies IoT Operations deployment and functionality +- โš ๏ธ **Creates billable Azure resources** - requires cleanup + +## Prerequisites + +### For Contract Tests + +**Required:** + +- Go 1.23 or later +- `terraform-docs` - Terraform contract validation +- Azure CLI with Bicep (`az bicep`) - Bicep contract validation + +**No Azure authentication required** - tests run entirely offline + +### For Deployment Tests + +**Required (in addition to contract test prerequisites):** + +- Terraform CLI (for Terraform deployment tests) +- Azure CLI authenticated (`az login`) +- Azure subscription with Owner or Contributor role +- Sufficient Azure quota for: + - Virtual machines (Standard_D4s_v3) + - IoT Operations resources + - Azure Arc enabled Kubernetes + +**Recommended:** + +- `kubectl` - Kubernetes cluster validation +- `jq` - JSON parsing in helper scripts + +## Setup + +### 1. Install Go Dependencies + +Run `go mod download` from the tests directory to fetch required Go packages. + +### 2. Install Required Tools + +**terraform-docs:** Install via package manager (Homebrew on macOS, manual download on Linux) + +**Azure Bicep:** Install via `az bicep install` + +**See:** Tool installation documentation in project root or official tool websites + +### 3. Configure Azure Credentials (Deployment Tests Only) + +Authenticate with `az login`, optionally set subscription with `az account set`, and verify with `az account show`. The deployment scripts automatically export `ARM_SUBSCRIPTION_ID`. + +**See:** [run-deployment-tests.sh](run-deployment-tests.sh) for automatic credential handling + +## Running Contract Tests + +Contract tests validate that all outputs defined in the `BlueprintOutputs` struct are properly declared in both Terraform and Bicep configurations. These tests run entirely offline and complete in seconds. + +### Using Helper Script (Recommended) + +Use `run-contract-tests.sh` with framework argument (`both`, `terraform`, or `bicep`) and optional `-v` flag for verbose output. + +**See:** [run-contract-tests.sh](run-contract-tests.sh) for script implementation + +### Using Go Test Directly + +Run `go test -v -run Contract` to execute all contract tests, or specify individual test names: + +- `TestTerraformOutputsContract` - Terraform validation +- `TestBicepOutputsContract` - Bicep validation + +**See:** [contract_terraform_test.go](contract_terraform_test.go) and [contract_bicep_test.go](contract_bicep_test.go) + +## Running Deployment Tests + +### Using the Helper Script (Recommended) + +The `run-deployment-tests.sh` script handles environment setup, auto-detection, and password generation. + +**Usage:** Framework argument (`terraform`, `bicep`, or `both`) with optional `-v` verbose flag + +**See:** [run-deployment-tests.sh](run-deployment-tests.sh) for complete implementation + +### Environment Variables + +**Auto-Detected Variables** (script handles these): + +- `ARM_SUBSCRIPTION_ID` - Azure subscription ID from `az account show` +- `CUSTOM_LOCATIONS_OID` - Custom Locations RP object ID from Azure AD +- `ADMIN_PASSWORD` - Auto-generated secure password (if not provided) + +**Configuration Variables** (override as needed): + +Set environment variables before running tests to customize behavior. See "Environment Variables Reference" section below for complete list with defaults. + +**Common overrides:** + +- `TEST_ENVIRONMENT`, `TEST_LOCATION`, `TEST_RESOURCE_PREFIX`, `TEST_INSTANCE` - Test naming +- `ADMIN_PASSWORD` - VM admin password (auto-generated if not set) +- `CLEANUP_RESOURCES` - Enable automatic resource deletion after test +- `SKIP_BICEP_DEPLOYMENT` + `BICEP_DEPLOYMENT_NAME` - Use existing deployment + +### Advanced Go Test Usage + +For advanced usage or CI/CD integration: + +```bash +# Initialize Azure subscription context +source ../../../scripts/az-sub-init.sh + +# Configure test parameters +export TEST_ENVIRONMENT="dev" +export TEST_LOCATION="eastus2" +export TEST_RESOURCE_PREFIX="t6" +export TEST_INSTANCE="001" + +# Terraform deployment test +export TEST_RESOURCE_GROUP_NAME="t6-terraform" +go test -v -run TestTerraformFullSingleNodeClusterDeploy -timeout 2h + +# Bicep deployment test (requires additional variables) +export TEST_RESOURCE_GROUP_NAME="t6-bicep" +export ADMIN_PASSWORD="MySecurePassword123!" +export CUSTOM_LOCATIONS_OID=$(az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv) +go test -v -run TestBicepFullSingleNodeClusterDeploy -timeout 2h +``` + +**Note**: Timeout of 2h allows for full deployment, validation, and optional cleanup. + +## Test Organization + +### Contract Tests (Static Analysis) + +- [contract_terraform_test.go](contract_terraform_test.go) - Terraform output contract validation using `terraform-docs` +- [contract_bicep_test.go](contract_bicep_test.go) - Bicep output contract validation using `az bicep build` + +### Deployment Tests (Integration) + +- [deploy_terraform_test.go](deploy_terraform_test.go) - Terraform deployment, validation, and optional cleanup +- [deploy_bicep_test.go](deploy_bicep_test.go) - Bicep deployment, validation, and optional cleanup + +### Shared Infrastructure + +- [outputs.go](outputs.go) - `BlueprintOutputs` struct defining the output contract +- [validation.go](validation.go) - Comprehensive validation test suites for deployed infrastructure +- [setup.go](setup.go) - Post-deployment configuration (Arc proxy, RBAC permissions) + +### Automation Scripts + +- [run-contract-tests.sh](run-contract-tests.sh) - Contract test runner with dependency checks +- [run-deployment-tests.sh](run-deployment-tests.sh) - Deployment test runner with auto-configuration + +## Test Files Reference + +| File | Purpose | Azure Auth Required | Deploys Resources | +|------------------------------|-----------------------------|---------------------|-------------------| +| `contract_terraform_test.go` | Validate Terraform outputs | (-) No | (-) No | +| `contract_bicep_test.go` | Validate Bicep outputs | (-) No | (-) No | +| `deploy_terraform_test.go` | Deploy & validate Terraform | (+) Yes | (+) Yes | +| `deploy_bicep_test.go` | Deploy & validate Bicep | (+) Yes | (+) Yes | +| `outputs.go` | Output contract definition | (-) No | (-) No | +| `validation.go` | Shared validation logic | (-) No | (-) No | +| `setup.go` | Post-deployment setup | (+) Yes | (-) No | + +## Important Notes + +### Cost Considerations + +- **Contract tests:** Free - no Azure resources created +- **Deployment tests:** Creates billable Azure resources including: + - Virtual machines (Standard_D4s_v3) + - Azure Arc enabled Kubernetes + - IoT Operations resources + - Storage accounts and networking + +**Always set `CLEANUP_RESOURCES=true` for cost control** or manually delete resources after testing. + +### Test Duration + +- **Contract tests:** < 1 second (static analysis only) +- **Deployment tests:** 30-45 minutes (full infrastructure deployment) + +### Resource Cleanup + +When `CLEANUP_RESOURCES=true`: + +- **Terraform:** Runs `terraform destroy` to remove all resources +- **Bicep:** Deletes entire resource group with `az group delete` + +**Manual cleanup** if tests fail during deployment: + +Use `az group list` to find test resource groups (typically prefixed with `t6-`), then delete with `az group delete --name --yes --no-wait`. + +## Troubleshooting Guide + +### Contract Test Issues + +**terraform-docs not found:** Install terraform-docs via package manager + +**az bicep not found:** Run `az bicep install` + +**Missing outputs:** Add to `outputs.tf` (Terraform) or `main.bicep` outputs (Bicep), or remove from `BlueprintOutputs` struct + +### Deployment Test Issues + +**Authentication errors:** Ensure `az login` completed successfully and verify with `az account show` + +**InsufficientQuota:** Request quota increase in Azure Portal or try different region via `TEST_LOCATION` variable + +**Timeout errors:** Increase test timeout using `-timeout` flag (e.g., `-timeout 3h`) + +**Cluster unreachable:** VM and Arc proxy setup handled automatically by [setup.go](setup.go); check Azure Portal for VM status +| `validation.go` | Validation test suites | โœ… Yes | โŒ No | +| `setup.go` | Arc proxy & permissions | โœ… Yes | โŒ No | + +## Validation Test Suite + +The [validation.go](validation.go) file contains comprehensive validation functions: + +### Core Infrastructure Validation + +Implemented in `validateCoreInfrastructure()`: + +- โœ… Deployment summary (resource group, location, environment) +- โœ… Security and identity (Key Vault, managed identities, RBAC) +- โœ… Observability (Log Analytics workspace, Grafana, Azure Monitor) +- โœ… Networking (NAT Gateway, virtual network, subnets) +- โœ… Data storage (Storage Account, Schema Registry) +- โœ… Container registry (Azure Container Registry) +- โœ… VM host resources (compute, networking, storage) +- โœ… Arc-connected cluster (Connected Cluster resource) + +### Kubernetes Cluster Validation + +Implemented in `validateKubernetesCluster()`: + +- โœ… Node status and readiness +- โœ… System namespaces and core pods +- โœ… Kubernetes services availability +- โœ… Resource quotas and limits + +### Azure IoT Operations Validation + +Implemented in `validateAzureIoTOperations()`: + +- โœ… AIO namespace existence and pod status +- โœ… Custom Location configuration +- โœ… Asset Endpoint Profile resources +- โœ… MQTT Broker deployment (Terraform deployments only) + +### Messaging Validation + +Implemented in `validateMessagingInfrastructure()` (Terraform only): + +- โœ… Event Hub message consumption and parsing +- โœ… Message format and schema validation +- โœ… End-to-end data flow verification + +## Key Differences: Terraform vs Bicep Tests + +| Feature | Terraform | Bicep | +|--------------------------|--------------------------------------------|-------------------------------------------| +| **Test Function** | `TestTerraformFullSingleNodeClusterDeploy` | `TestBicepFullSingleNodeClusterDeploy` | +| **Validation Function** | `validateTerraformDeployment` | `validateBicepDeployment` | +| **Messaging Tests** | (+) Included (Event Hub validation) | (-) Skipped (dataflow config limitations) | +| **Password Requirement** | (-) Not required (uses SSH keys) | (+) Required (auto-generated by script) | +| **Custom Locations OID** | (-) Not required (uses managed identity) | (+) Required (auto-detected by script) | +| **Deployment Scope** | Resource group | Subscription-level | + +## Environment Variables Reference + +### Auto-Detected Variables + +| Variable | Description | Fallback | +|------------------------|------------------------------------|------------------------------| +| `ARM_SUBSCRIPTION_ID` | Azure subscription ID | Script fails if not detected | +| `CUSTOM_LOCATIONS_OID` | Custom Locations service principal | Script fails if not detected | +| `ADMIN_PASSWORD` | VM admin password | Auto-generated password | + +### Configuration Variables + +| Variable | Description | Default | +|----------------------------|----------------------|-----------------------------------------| +| `TEST_ENVIRONMENT` | Environment name | `dev` | +| `TEST_LOCATION` | Azure region | `eastus2` | +| `TEST_RESOURCE_PREFIX` | Resource name prefix | `t6` | +| `TEST_INSTANCE` | Instance identifier | `001` | +| `TEST_RESOURCE_GROUP_NAME` | Resource group name | `${TEST_RESOURCE_PREFIX}-{tf or bicep}` | + +### Optional Control Variables + +| Variable | Description | Default | +|-------------------------|----------------------------------|-------------------------| +| `CLEANUP_RESOURCES` | Auto-delete resources after test | `false` | +| `SKIP_BICEP_DEPLOYMENT` | Skip deployment, use existing | `false` | +| `BICEP_DEPLOYMENT_NAME` | Name of existing deployment | `bicep-deployment-test` | + +**โš ๏ธ Resource Cleanup Note**: By default, tests **DO NOT** delete resources after completion. Set `CLEANUP_RESOURCES=true` to enable automatic cleanup. + +## Common Workflows + +### Development Workflow + +**Typical development cycle for test improvements:** + +```bash +# Step 1: Validate IaC output contract (fast, no costs) +./run-contract-tests.sh both + +# Step 2: Initial deployment (resources remain for inspection) +./run-deployment-tests.sh terraform + +# Step 3: Iterate on validation logic without redeploying +export SKIP_BICEP_DEPLOYMENT=true +go test -v -run TestBicepFullSingleNodeClusterDeploy -timeout 30m + +# Step 4: Test cleanup functionality before final commit +export CLEANUP_RESOURCES=true +./run-deployment-tests.sh terraform +``` + +### CI/CD Integration + +**Recommended pipeline stages:** + +```bash +# Stage 1: PR validation (every commit, ~30 seconds) +./run-contract-tests.sh both + +# Stage 2: Nightly integration tests (scheduled, ~90 minutes) +export CLEANUP_RESOURCES=true +./run-deployment-tests.sh both +``` + +### Adding New Infrastructure Outputs + +**Complete workflow for adding new outputs:** + +```bash +# Step 1: Add output to IaC configuration +# - For Terraform: Edit ../terraform/outputs.tf +# - For Bicep: Edit ../bicep/main.bicep + +# Step 2: Add corresponding field to BlueprintOutputs struct +# - Edit outputs.go +# - Add struct field with terraform:"/" or bicep:"/" tag + +# Step 3: Verify contract validation passes +./run-contract-tests.sh both + +# Step 4: Add validation logic (if needed) +# - Edit validation.go +# - Add assertions for new output values + +# Step 5: Run end-to-end deployment test +./run-deployment-tests.sh terraform +``` + +### Debugging Failed Deployments + +**Troubleshooting and inspection workflow:** + +```bash +# Deploy without cleanup to inspect resources +export CLEANUP_RESOURCES=false +./run-deployment-tests.sh terraform + +# Manually inspect resources in Azure Portal or CLI +az resource list --resource-group t6-terraform -o table + +# Test specific validation functions +go test -v -run TestTerraformFullSingleNodeClusterDeploy/Validate -timeout 30m + +# Clean up when done +export CLEANUP_RESOURCES=true +go test -v -run TestTerraformFullSingleNodeClusterDeploy -timeout 15m +``` + +## Troubleshooting + +### Contract Test Failures + +```text +โŒ Missing 2 required outputs in terraform: [new_output_1, new_output_2] +``` + +**Solution**: Add the missing outputs to `terraform/outputs.tf` or `bicep/main.bicep`, or remove them from the `BlueprintOutputs` struct in `outputs.go`. + +### Auto-Detection Failures + +If `ARM_SUBSCRIPTION_ID` auto-detection fails: + +```bash +az login +export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) +``` + +If `CUSTOM_LOCATIONS_OID` auto-detection fails: + +```bash +# Ensure you have permissions to query Azure AD +export CUSTOM_LOCATIONS_OID=$(az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv) +``` + +### Arc Proxy Connection Issues + +If Arc proxy fails to connect: + +```bash +# Manually test Arc connectivity +az connectedk8s proxy \ + --name \ + --resource-group & + +# Wait a few seconds, then test +kubectl get nodes +``` + +### Permission Issues + +Required Azure permissions: + +- **Contributor** access to subscription +- **Azure AD read** permissions (for Custom Locations OID query) +- **Event Hub Data Receiver** role (auto-assigned by test setup) + +## Cost Management + +**โš ๏ธ WARNING**: Deployment tests create real Azure resources and incur costs! + +### Cost Optimization Strategies + +1. **Use contract tests for rapid iteration** - Zero cost, instant feedback +2. **Reuse deployments** - Set `SKIP_BICEP_DEPLOYMENT=true` after initial deployment +3. **Clean up after testing** - Tests don't auto-destroy resources +4. **Use smaller configurations** - Modify test variables to use minimal SKUs +5. **Monitor Azure costs** - Track spending during test development + +### Estimated Costs + +Approximate costs per deployment test run (varies by region): + +- VM hosts: ~$2-5/hour +- Azure IoT Operations: ~$1-2/hour +- Networking (NAT Gateway): ~$0.50/hour +- Storage and monitoring: ~$0.25/hour + +**Total**: ~$4-8/hour per deployment + +## Additional Resources + +- [Blueprint README](../README.md) - Complete blueprint documentation and deployment guide +- [Test Utilities Package](../../../src/900-tools-utilities/904-test-utilities/) - Shared testing utilities and functions +- [Terraform Implementation](../terraform/) - Terraform IaC configuration +- [Bicep Implementation](../bicep/) - Bicep IaC configuration +- [Terratest Documentation](https://terratest.gruntwork.io/) - Underlying test framework documentation diff --git a/blueprints/full-single-node-cluster/tests/contract_bicep_test.go b/blueprints/full-single-node-cluster/tests/contract_bicep_test.go new file mode 100644 index 00000000..32340596 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/contract_bicep_test.go @@ -0,0 +1,37 @@ +package test + +import ( + "testing" + + testutil "github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities" +) + +// TestBicepOutputsContract validates Bicep output declarations against test requirements. +// +// Purpose: +// +// Ensures all outputs defined in BlueprintOutputs struct are declared in bicep/main.bicep +// +// Behavior: +// - Static analysis only - no deployment or Azure authentication required +// - Uses az bicep build to compile Bicep to ARM template +// - Extracts outputs from compiled ARM template JSON +// - Automatically converts required output names from snake_case to camelCase +// since Bicep uses camelCase convention while test structs use snake_case tags +// - Compares against BlueprintOutputs.GetRequiredOutputKeys() (converted to camelCase) +// - Fails if any required outputs are missing from main.bicep +// +// Naming Convention: +// - BlueprintOutputs uses snake_case tags (e.g., "deployment_summary") +// - Bicep outputs must use camelCase (e.g., "deploymentSummary") +// - ValidateBicepContract handles the automatic conversion +// +// Requirements: +// - Azure CLI with Bicep extension (az bicep install) +// - Valid Bicep configuration in ../bicep +// +// Runtime: Typically completes in < 2 seconds +func TestBicepOutputsContract(t *testing.T) { + var blueprint BlueprintOutputs + testutil.ValidateBicepContract(t, "../bicep", blueprint.GetRequiredOutputKeys()) +} diff --git a/blueprints/full-single-node-cluster/tests/contract_terraform_test.go b/blueprints/full-single-node-cluster/tests/contract_terraform_test.go new file mode 100644 index 00000000..98cec781 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/contract_terraform_test.go @@ -0,0 +1,29 @@ +package test + +import ( + "testing" + + testutil "github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities" +) + +// TestTerraformOutputsContract validates Terraform output declarations against test requirements. +// +// Purpose: +// +// Ensures all outputs defined in BlueprintOutputs struct are declared in terraform/outputs.tf +// +// Behavior: +// - Static analysis only - no deployment or Azure authentication required +// - Uses terraform-docs to extract output declarations +// - Compares against BlueprintOutputs.GetRequiredOutputKeys() +// - Fails if any required outputs are missing from outputs.tf +// +// Requirements: +// - terraform-docs must be installed (brew install terraform-docs) +// - Valid Terraform configuration in ../terraform +// +// Runtime: Typically completes in < 1 second +func TestTerraformOutputsContract(t *testing.T) { + var blueprint BlueprintOutputs + testutil.ValidateTerraformContract(t, "../terraform", blueprint.GetRequiredOutputKeys()) +} diff --git a/blueprints/full-single-node-cluster/tests/deploy_bicep_test.go b/blueprints/full-single-node-cluster/tests/deploy_bicep_test.go new file mode 100644 index 00000000..8a068cd8 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/deploy_bicep_test.go @@ -0,0 +1,98 @@ +package test + +import ( + "os" + "testing" + + testutil "github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities" + "github.com/stretchr/testify/require" +) + +// TestBicepFullSingleNodeClusterDeploy performs full Bicep deployment and validation. +// +// WARNING: Deploys real Azure resources that incur costs. +// +// Configuration via environment variables: +// +// Required (always): +// ARM_SUBSCRIPTION_ID, TEST_RESOURCE_GROUP_NAME, TEST_ENVIRONMENT, +// TEST_LOCATION, TEST_RESOURCE_PREFIX, TEST_INSTANCE +// +// Required (for deployment): +// ADMIN_PASSWORD, CUSTOM_LOCATIONS_OID +// +// Optional: +// SKIP_BICEP_DEPLOYMENT=true (use existing deployment outputs) +// BICEP_DEPLOYMENT_NAME= (defaults to "bicep-deployment-test") +func TestBicepFullSingleNodeClusterDeploy(t *testing.T) { + t.Parallel() + + bicepDir := "../bicep" + + // Get subscription ID for deployment + subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") + require.NotEmpty(t, subscriptionID, "ARM_SUBSCRIPTION_ID environment variable must be set") + + // Check if we should skip deployment and use existing + skipDeployment := os.Getenv("SKIP_BICEP_DEPLOYMENT") == "true" + deploymentName := os.Getenv("BICEP_DEPLOYMENT_NAME") + if deploymentName == "" { + deploymentName = "bicep-deployment-test" + } + + // Get test configuration from environment variables (set in run-deployment-tests.sh) + resourceGroupName := os.Getenv("TEST_RESOURCE_GROUP_NAME") + require.NotEmpty(t, resourceGroupName, "TEST_RESOURCE_GROUP_NAME environment variable must be set") + + environment := os.Getenv("TEST_ENVIRONMENT") + require.NotEmpty(t, environment, "TEST_ENVIRONMENT environment variable must be set") + + location := os.Getenv("TEST_LOCATION") + require.NotEmpty(t, location, "TEST_LOCATION environment variable must be set") + + resourcePrefix := os.Getenv("TEST_RESOURCE_PREFIX") + require.NotEmpty(t, resourcePrefix, "TEST_RESOURCE_PREFIX environment variable must be set") + + instance := os.Getenv("TEST_INSTANCE") + require.NotEmpty(t, instance, "TEST_INSTANCE environment variable must be set") + + var rawOutputs map[string]any + + if skipDeployment { + // Use existing deployment - no password/OID required + t.Logf("Skipping deployment, fetching outputs from existing deployment: %s", deploymentName) + rawOutputs = testutil.GetBicepOutputs(t, deploymentName) + } else { + // Run full deployment + adminPassword := os.Getenv("ADMIN_PASSWORD") + require.NotEmpty(t, adminPassword, "ADMIN_PASSWORD environment variable must be set") + + customLocationsOid := os.Getenv("CUSTOM_LOCATIONS_OID") + require.NotEmpty(t, customLocationsOid, "CUSTOM_LOCATIONS_OID environment variable must be set") + + // Required parameters for deployment (Bicep format) + params := map[string]any{ + "common": map[string]any{ + "environment": environment, + "location": location, + "resourcePrefix": resourcePrefix, + "instance": instance, + }, + "resourceGroupName": resourceGroupName, + "adminPassword": adminPassword, + "customLocationsOid": customLocationsOid, + } + + // Check if resources should be cleaned up after test (defaults to false) + shouldCleanup := os.Getenv("CLEANUP_RESOURCES") == "true" + + // Deploy using Bicep and get outputs + rawOutputs = testutil.DeployBicep(t, bicepDir, subscriptionID, deploymentName, resourceGroupName, params, shouldCleanup) + } + + // Parse outputs and setup permissions/proxy + ctx := prepareBicepOutputsAndSetup(t, rawOutputs, subscriptionID, resourceGroupName) + + // Validate deployment and run all tests + validateDeploymentWithoutMessaging(t, ctx, subscriptionID, resourceGroupName, resourcePrefix, location) +} diff --git a/blueprints/full-single-node-cluster/tests/deploy_terraform_test.go b/blueprints/full-single-node-cluster/tests/deploy_terraform_test.go new file mode 100644 index 00000000..99ef88ac --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/deploy_terraform_test.go @@ -0,0 +1,62 @@ +package test + +import ( + "os" + "testing" + + testutil "github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities" + "github.com/stretchr/testify/require" +) + +// TestTerraformFullSingleNodeClusterDeploy performs full Terraform deployment and validation. +// +// WARNING: Deploys real Azure resources that incur costs. +// +// Configuration via environment variables: +// +// Required: +// ARM_SUBSCRIPTION_ID, TEST_ENVIRONMENT, TEST_LOCATION, +// TEST_RESOURCE_PREFIX, TEST_RESOURCE_GROUP_NAME +func TestTerraformFullSingleNodeClusterDeploy(t *testing.T) { + t.Parallel() + + terraformDir := "../terraform" + + // Get test configuration from environment variables (set in run-deployment-tests.sh) + environment := os.Getenv("TEST_ENVIRONMENT") + require.NotEmpty(t, environment, "TEST_ENVIRONMENT environment variable must be set") + + location := os.Getenv("TEST_LOCATION") + require.NotEmpty(t, location, "TEST_LOCATION environment variable must be set") + + resourcePrefix := os.Getenv("TEST_RESOURCE_PREFIX") + require.NotEmpty(t, resourcePrefix, "TEST_RESOURCE_PREFIX environment variable must be set") + + resourceGroupName := os.Getenv("TEST_RESOURCE_GROUP_NAME") + require.NotEmpty(t, resourceGroupName, "TEST_RESOURCE_GROUP_NAME environment variable must be set") + + // Required variables for deployment + vars := map[string]any{ + "environment": environment, + "location": location, + "resource_prefix": resourcePrefix, + "resource_group_name": resourceGroupName, + "should_enable_opc_ua_simulator": true, + } + + // Get subscription ID for Azure module functions + subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") + require.NotEmpty(t, subscriptionID, "ARM_SUBSCRIPTION_ID environment variable must be set") + + // Check if resources should be cleaned up after test (defaults to false) + shouldCleanup := os.Getenv("CLEANUP_RESOURCES") == "true" + + // Deploy using Terraform and get outputs + rawOutputs := testutil.DeployTerraform(t, terraformDir, vars, shouldCleanup) + + // Parse outputs and setup permissions/proxy + ctx := prepareTerraformOutputsAndSetup(t, rawOutputs, subscriptionID, resourceGroupName) + + // Validate deployment and run all tests (including Event Hub validation for Terraform) + validateDeploymentWithMessaging(t, ctx, subscriptionID, resourceGroupName, resourcePrefix, location) +} diff --git a/blueprints/full-single-node-cluster/tests/go.mod b/blueprints/full-single-node-cluster/tests/go.mod new file mode 100644 index 00000000..ec26cd71 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/go.mod @@ -0,0 +1,174 @@ +module github.com/microsoft/edge-ai/blueprints/full-single-node-cluster/tests + +go 1.25.5 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 + github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 + github.com/google/uuid v1.6.0 + github.com/gruntwork-io/terratest v0.54.0 + github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities v0.0.0-00010101000000-000000000000 + github.com/microsoftgraph/msgraph-sdk-go v1.93.0 + github.com/stretchr/testify v1.11.1 + k8s.io/api v0.34.0 + k8s.io/apimachinery v0.34.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/azure-sdk-for-go v51.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3 v3.0.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect + github.com/Azure/go-amqp v1.4.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.20 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 // indirect + github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.38.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 // indirect + github.com/aws/aws-sdk-go-v2/service/lambda v1.69.0 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.33.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/smithy-go v1.22.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/gruntwork-io/go-commons v0.8.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-getter/v2 v2.2.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.22.0 // indirect + github.com/hashicorp/terraform-json v0.23.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.5 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect + github.com/microsoft/kiota-abstractions-go v1.9.3 // indirect + github.com/microsoft/kiota-authentication-azure-go v1.3.1 // indirect + github.com/microsoft/kiota-http-go v1.5.4 // indirect + github.com/microsoft/kiota-serialization-form-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-json-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-multipart-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-text-go v1.1.3 // indirect + github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pquerna/otp v1.4.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 // indirect + github.com/tmccombs/hcl2json v0.6.4 // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + github.com/urfave/cli v1.22.16 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/zclconf/go-cty v1.15.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/client-go v0.34.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities => ../../../src/900-tools-utilities/904-test-utilities diff --git a/blueprints/full-single-node-cluster/tests/go.sum b/blueprints/full-single-node-cluster/tests/go.sum new file mode 100644 index 00000000..3e09cad3 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/go.sum @@ -0,0 +1,470 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go v51.0.0+incompatible h1:p7blnyJSjJqf5jflHbSGhIhEpXIgIFmYZNg5uwqweso= +github.com/Azure/azure-sdk-for-go v51.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0 h1:BwmN55GUUfwFPSd44bxBVkFD8yJAp+LLjGRjSnpbeUM= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0/go.mod h1:OowfWwCcXlcn1Nkk6oTxeCuGNRElKtYpzkF1/gZ42Ig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3 v3.0.0 h1:NYYoOOPGOqUXw/bGIVd6OY/K8J23a18IAlAx1tOHWNo= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3 v3.0.0/go.mod h1:LDN3sr8FJ36sY6ZmMes6Q2vHJ+5r1aFsE3wEo7VbXJg= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0 h1:4hGvxD72TluuFIXVr8f4XkKZfqAa7Pj61t0jmQ7+kes= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0/go.mod h1:TSH7DcFItwAufy0Lz+Ft2cyopExCpxbOxI5SkH4dRNo= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= +github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4= +github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.20 h1:s8H1PbCZSqg/DH7JMlOz6YMig6htWLNPsjDdlLqCx3M= +github.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 h1:TzPg6B6fTZ0G1zBf3T54aI7p3cAT6u//TOXGPmFMOXg= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= +github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= +github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= +github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 h1:hqcxMc2g/MwwnRMod9n6Bd+t+9Nf7d5qRg7RaXKPd6o= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41/go.mod h1:d1eH0VrttvPmrCraU68LOyNdu26zFxQFjrVSb5vdhog= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg= +github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 h1:fDg0RlN30Xf/yYzEUL/WXqhmgFsjVb/I3230oCfyI5w= +github.com/aws/aws-sdk-go-v2/service/acm v1.30.6/go.mod h1:zRR6jE3v/TcbfO8C2P+H0Z+kShiKKVaVyoIl8NQRjyg= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0 h1:1KzQVZi7OTixxaVJ8fWaJAUBjme+iQ3zBOCZhE4RgxQ= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0/go.mod h1:I1+/2m+IhnK5qEbhS3CrzjeiVloo9sItE/2K+so0fkU= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0 h1:OREVd94+oXW5a+3SSUAo4K0L5ci8cucCLu+PSiek8OU= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0/go.mod h1:Qbr4yfpNqVNl69l/GEDK+8wxLf/vHi0ChoiSDzD7thU= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 h1:vucMirlM6D+RDU8ncKaSZ/5dGrXNajozVwpmWNPn2gQ= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1/go.mod h1:fceORfs010mNxZbQhfqUjUeHlTwANmIT4mvHamuUaUg= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 h1:RhSoBFT5/8tTmIseJUXM6INTXTQDF8+0oyxWBnozIms= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 h1:zg+3FGHA0PBs0KM25qE/rOf2o5zsjNa1g/Qq83+SDI0= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6/go.mod h1:ZSq54Z9SIsOTf1Efwgw1msilSs4XVEfVQiP9nYVnKpM= +github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0 h1:7/vgFWplkusJN/m+3QOa+W9FNRqa8ujMPNmdufRaJpg= +github.com/aws/aws-sdk-go-v2/service/ecs v1.52.0/go.mod h1:dPTOvmjJQ1T7Q+2+Xs2KSPrMvx+p0rpyV+HsQVnUK4o= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.1 h1:hfkzDZHBp9jAT4zcd5mtqckpU4E3Ax0LQaEWWk1VgN8= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.1/go.mod h1:u36ahDtZcQHGmVm/r+0L1sfKX4fzLEMdCqiKRKkUMVM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 h1:3Y457U2eGukmjYjeHG6kanZpDzJADa2m0ADqnuePYVQ= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5/go.mod h1:CfwEHGkTjYZpkQ/5PvcbEtT7AJlG68KkEvmtwU8z3/U= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 h1:CZImQdb1QbU9sGgJ9IswhVkxAcjkkD1eQTMA1KHWk+E= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.6/go.mod h1:YJDdlK0zsyxVBxGU48AR/Mi8DMrGdc1E3Yij4fNrONA= +github.com/aws/aws-sdk-go-v2/service/lambda v1.69.0 h1:BXt75frE/FYtAmEDBJRBa2HexOw+oAZWZl6QknZEFgg= +github.com/aws/aws-sdk-go-v2/service/lambda v1.69.0/go.mod h1:guz2K3x4FKSdDaoeB+TPVgJNU9oj2gftbp5cR8ela1A= +github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 h1:eqHz3Uih+gb0vLE5Cc4Xf733vOxsxDp6GFUUVQU4d7w= +github.com/aws/aws-sdk-go-v2/service/rds v1.91.0/go.mod h1:h2jc7IleH3xHY7y+h8FH7WAZcz3IVLOB6/jXotIQ/qU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 h1:wmt05tPp/CaRZpPV5B4SaJ5TwkHKom07/BzHoLdkY1o= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2/go.mod h1:d+K9HESMpGb1EU9/UmmpInbGIUcAkwmcY6ZO/A3zZsw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 h1:Q2ax8S21clKOnHhhr933xm3JxdJebql+R7aNo7p7GBQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 h1:1KDMKvOKNrpD667ORbZ/+4OgvUoaok1gg/MLzrHF9fw= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6/go.mod h1:DmtyfCfONhOyVAJ6ZMTrDSFIeyCBlEO93Qkfhxwbxu0= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.6 h1:lEUtRHICiXsd7VRwRjXaY7MApT2X4Ue0Mrwe6XbyBro= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.6/go.mod h1:SODr0Lu3lFdT0SGsGX1TzFTapwveBrT5wztVoYtppm8= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 h1:39WvSrVq9DD6UHkD+fx5x19P5KpRQfNdtgReDVNbelc= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1/go.mod h1:3gwPzC9LER/BTQdQZ3r6dUktb1rSjABF1D3Sr6nS7VU= +github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 h1:mADKqoZaodipGgiZfuAjtlcr4IVBtXPZKVjkzUZCCYM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0/go.mod h1:l9qF25TzH95FhcIak6e4vt79KE4I7M2Nf59eMUVjj6c= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU= +github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro= +github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78= +github.com/gruntwork-io/terratest v0.54.0 h1:JOVATYDpU0NAPbEkgYUP50BR2m45UGiR4dbs20sKzck= +github.com/gruntwork-io/terratest v0.54.0/go.mod h1:QvwQWZMTJmJB4E0d1Uc18quQm7+X53liKKp+fJSuaKA= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk= +github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= +github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= +github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= +github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 h1:ofNAzWCcyTALn2Zv40+8XitdzCgXY6e9qvXwN9W0YXg= +github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= +github.com/microsoft/kiota-abstractions-go v1.9.3 h1:cqhbqro+VynJ7kObmo7850h3WN2SbvoyhypPn8uJ1SE= +github.com/microsoft/kiota-abstractions-go v1.9.3/go.mod h1:f06pl3qSyvUHEfVNkiRpXPkafx7khZqQEb71hN/pmuU= +github.com/microsoft/kiota-authentication-azure-go v1.3.1 h1:AGta92S6IL1E6ZMDb8YYB7NVNTIFUakbtLKUdY5RTuw= +github.com/microsoft/kiota-authentication-azure-go v1.3.1/go.mod h1:26zylt2/KfKwEWZSnwHaMxaArpbyN/CuzkbotdYXF0g= +github.com/microsoft/kiota-http-go v1.5.4 h1:wSUmL1J+bTQlAWHjbRkSwr+SPAkMVYeYxxB85Zw0KFs= +github.com/microsoft/kiota-http-go v1.5.4/go.mod h1:L+5Ri+SzwELnUcNA0cpbFKp/pBbvypLh3Cd1PR6sjx0= +github.com/microsoft/kiota-serialization-form-go v1.1.2 h1:SD6MATqNw+Dc5beILlsb/D87C36HKC/Zw7l+N9+HY2A= +github.com/microsoft/kiota-serialization-form-go v1.1.2/go.mod h1:m4tY2JT42jAZmgbqFwPy3zGDF+NPJACuyzmjNXeuHio= +github.com/microsoft/kiota-serialization-json-go v1.1.2 h1:eJrPWeQ665nbjO0gsHWJ0Bw6V/ZHHU1OfFPaYfRG39k= +github.com/microsoft/kiota-serialization-json-go v1.1.2/go.mod h1:deaGt7fjZarywyp7TOTiRsjfYiyWxwJJPQZytXwYQn8= +github.com/microsoft/kiota-serialization-multipart-go v1.1.2 h1:1pUyA1QgIeKslQwbk7/ox1TehjlCUUT3r1f8cNlkvn4= +github.com/microsoft/kiota-serialization-multipart-go v1.1.2/go.mod h1:j2K7ZyYErloDu7Kuuk993DsvfoP7LPWvAo7rfDpdPio= +github.com/microsoft/kiota-serialization-text-go v1.1.3 h1:8z7Cebn0YAAr++xswVgfdxZjnAZ4GOB9O7XP4+r5r/M= +github.com/microsoft/kiota-serialization-text-go v1.1.3/go.mod h1:NDSvz4A3QalGMjNboKKQI9wR+8k+ih8UuagNmzIRgTQ= +github.com/microsoftgraph/msgraph-sdk-go v1.93.0 h1:H4WgYetjWYpr79S2ylNJchFDQTBE0ldKx8sR0qxyRGQ= +github.com/microsoftgraph/msgraph-sdk-go v1.93.0/go.mod h1:sue5+4Z9FCOon6pHgvC1djjybs9ZYB3LZaAGYI1Qcfo= +github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0 h1:0SrIoFl7TQnMRrsi5TFaeNe0q8KO5lRzRp4GSCCL2So= +github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0/go.mod h1:A1iXs+vjsRjzANxF6UeKv2ACExG7fqTwHHbwh1FL+EE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 h1:7hth9376EoQEd1hH4lAp3vnaLP2UMyxuMMghLKzDHyU= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVLGw= +github.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/blueprints/full-single-node-cluster/tests/outputs.go b/blueprints/full-single-node-cluster/tests/outputs.go new file mode 100644 index 00000000..c197d017 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/outputs.go @@ -0,0 +1,57 @@ +// Package test provides validation and testing utilities for the full-single-node-cluster blueprint. +// This file defines the strongly-typed output contract between IaC and tests. +package test + +import ( + "testing" + + testutil "github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities" +) + +// BlueprintOutputs defines the complete contract between IaC outputs and test code. +// Every field must have an `output` tag matching the IaC output name. +// To add a new output: add field with tag, then declare output in both terraform/outputs.tf and bicep/main.bicep. +// Contract tests (terraform_outputs_contract_test.go and bicep_outputs_contract_test.go) enforce this at compile time. +type BlueprintOutputs struct { + DeploymentSummary map[string]any `output:"deployment_summary"` + SecurityIdentity map[string]any `output:"security_identity"` + Observability map[string]any `output:"observability"` + NatGateway any `output:"nat_gateway"` + NatGatewayPublicIps any `output:"nat_gateway_public_ips"` + DataStorage map[string]any `output:"data_storage"` + ContainerRegistry map[string]any `output:"container_registry"` + Messaging map[string]any `output:"messaging"` + VmHost any `output:"vm_host"` + ArcConnectedCluster map[string]any `output:"arc_connected_cluster"` + ClusterConnection map[string]any `output:"cluster_connection"` + AzureIotOperations map[string]any `output:"azure_iot_operations"` + Assets map[string]any `output:"assets"` + ACRNetworkPosture any `output:"acr_network_posture"` +} + +// ParseTerraformBlueprintOutputs converts raw Terraform deployment outputs to strongly-typed BlueprintOutputs. +// Uses reflection to map outputs based on struct field tags. +// Fails test immediately if any required output is missing from raw outputs. +// Note: This validates presence of output keys, not their values. Outputs can be nil/null. +func ParseTerraformBlueprintOutputs(t *testing.T, raw map[string]any) *BlueprintOutputs { + outputs := &BlueprintOutputs{} + testutil.ParseTerraformOutputsFromMap(t, raw, outputs) + return outputs +} + +// ParseBicepBlueprintOutputs converts raw Bicep deployment outputs to strongly-typed BlueprintOutputs. +// Bicep outputs use camelCase while struct tags use snake_case. +// Uses reflection to map outputs based on struct field tags after converting to camelCase. +// Fails test immediately if any required output is missing from raw outputs. +func ParseBicepBlueprintOutputs(t *testing.T, raw map[string]any) *BlueprintOutputs { + outputs := &BlueprintOutputs{} + testutil.ParseBicepOutputsFromMap(t, raw, outputs) + return outputs +} + +// GetRequiredOutputKeys returns all output names defined in BlueprintOutputs struct. +// Uses reflection on struct tags to extract output names. +// Used by contract tests to validate IaC output declarations. +func (BlueprintOutputs) GetRequiredOutputKeys() []string { + return testutil.GetOutputKeysFromStruct(BlueprintOutputs{}) +} diff --git a/blueprints/full-single-node-cluster/tests/run-contract-tests.sh b/blueprints/full-single-node-cluster/tests/run-contract-tests.sh new file mode 100755 index 00000000..1d592400 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/run-contract-tests.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# Helper script to run contract tests for Terraform and Bicep +# Contract tests validate IaC outputs without deployment (fast, $0 cost) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_usage() { + cat << EOF +Usage: $0 [terraform|bicep|both] [options] + +Run static contract tests to validate IaC outputs match test requirements. +These tests run in seconds without deploying any Azure resources. + +Arguments: + terraform Run only Terraform contract test + bicep Run only Bicep contract test + both Run both tests (default) + +Options: + -v, --verbose Enable verbose test output + -h, --help Show this help message + +Examples: + $0 # Run both contract tests + $0 terraform # Run only Terraform contract test + $0 bicep -v # Run only Bicep with verbose output + +Contract Tests: + โœ“ Terraform: Validates outputs.tf using terraform-docs + โœ“ Bicep: Validates main.bicep outputs using az bicep build + โœ“ Fast: Complete in <5 seconds + โœ“ Zero Cost: No Azure resources deployed + +Dependencies: + - terraform-docs (brew install terraform-docs) + - az bicep (az bicep install) + - Go toolchain (go test) +EOF +} + +# Parse arguments +TEST_TYPE="both" +VERBOSE_FLAG="" + +while [[ $# -gt 0 ]]; do + case $1 in + terraform|bicep|both) + TEST_TYPE="$1" + shift + ;; + -v|--verbose) + VERBOSE_FLAG="-v" + shift + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + print_usage + exit 1 + ;; + esac +done + +echo -e "${BLUE}โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—${NC}" +echo -e "${BLUE}โ•‘ Contract Tests - Static Validation โ•‘${NC}" +echo -e "${BLUE}โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" +echo "" +echo -e "${GREEN}Test Type:${NC} $TEST_TYPE" +echo -e "${GREEN}Directory:${NC} $(basename "$(dirname "$SCRIPT_DIR")")/tests" +echo "" + +# Check dependencies +echo -e "${YELLOW}Checking dependencies...${NC}" + +# Check Go +if ! command -v go &> /dev/null; then + echo -e "${RED}โœ— Go not found. Please install Go toolchain.${NC}" + exit 1 +fi +echo -e "${GREEN}โœ“ Go: $(go version | awk '{print $3}')${NC}" + +# Check terraform-docs +if [[ "$TEST_TYPE" == "terraform" || "$TEST_TYPE" == "both" ]]; then + if ! command -v terraform-docs &> /dev/null; then + echo -e "${RED}โœ— terraform-docs not found${NC}" + echo -e "${YELLOW} Install: brew install terraform-docs${NC}" + exit 1 + fi + echo -e "${GREEN}โœ“ terraform-docs: $(terraform-docs version | head -n1)${NC}" +fi + +# Check az bicep +if [[ "$TEST_TYPE" == "bicep" || "$TEST_TYPE" == "both" ]]; then + if ! command -v az &> /dev/null; then + echo -e "${RED}โœ— Azure CLI not found${NC}" + echo -e "${YELLOW} Install: https://docs.microsoft.com/cli/azure/install-azure-cli${NC}" + exit 1 + fi + + # Check bicep is installed + if ! az bicep version &> /dev/null; then + echo -e "${RED}โœ— Bicep not installed${NC}" + echo -e "${YELLOW} Install: az bicep install${NC}" + exit 1 + fi +fi + +echo "" + +# Run tests +EXIT_CODE=0 + +run_test() { + local test_name=$1 + local test_pattern=$2 + + echo -e "${BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${NC}" + echo -e "${YELLOW}Running: $test_name${NC}" + echo -e "${BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${NC}" + + if go test $VERBOSE_FLAG -run "$test_pattern" .; then + echo -e "${GREEN}โœ“ $test_name PASSED${NC}" + else + echo -e "${RED}โœ— $test_name FAILED${NC}" + EXIT_CODE=1 + fi + echo "" +} + +case $TEST_TYPE in + terraform) + run_test "Terraform Contract Test" "TestTerraformOutputsContract" + ;; + bicep) + run_test "Bicep Contract Test" "TestBicepOutputsContract" + ;; + both) + run_test "Terraform Contract Test" "TestTerraformOutputsContract" + run_test "Bicep Contract Test" "TestBicepOutputsContract" + ;; +esac + +# Summary +echo -e "${BLUE}โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—${NC}" +if [[ $EXIT_CODE -eq 0 ]]; then + echo -e "${BLUE}โ•‘${GREEN} All Tests PASSED โœ“ ${BLUE}โ•‘${NC}" +else + echo -e "${BLUE}โ•‘${RED} Some Tests FAILED โœ— ${BLUE}โ•‘${NC}" +fi +echo -e "${BLUE}โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" + +exit $EXIT_CODE diff --git a/blueprints/full-single-node-cluster/tests/run-deployment-tests.sh b/blueprints/full-single-node-cluster/tests/run-deployment-tests.sh new file mode 100755 index 00000000..95d6e856 --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/run-deployment-tests.sh @@ -0,0 +1,163 @@ +#!/bin/bash +# Helper script to run deployment tests for both Terraform and Bicep + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +print_usage() { + echo "Usage: $0 [terraform|bicep|both] [options]" + echo "" + echo "Arguments:" + echo " terraform Run only Terraform deployment tests" + echo " bicep Run only Bicep deployment tests" + echo " both Run both Terraform and Bicep tests (default)" + echo "" + echo "Options:" + echo " -v, --verbose Enable verbose test output" + echo " -h, --help Show this help message" + echo "" + echo "Environment Variables:" + echo " ARM_SUBSCRIPTION_ID Azure subscription ID (auto-detected if not set)" + echo " ADMIN_PASSWORD (Required for Bicep) VM admin password" + echo " CUSTOM_LOCATIONS_OID Custom Locations OID (auto-detected if not set)" + echo "" + echo "Examples:" + echo " $0 terraform" + echo " $0 bicep -v" + echo " $0 both" +} + +# Parse arguments +DEPLOYMENT_TYPE="both" +VERBOSE_FLAG="" + +while [[ $# -gt 0 ]]; do + case $1 in + terraform|bicep|both) + DEPLOYMENT_TYPE="$1" + shift + ;; + -v|--verbose) + VERBOSE_FLAG="-v" + shift + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + print_usage + exit 1 + ;; + esac +done + +# Auto-detect ARM_SUBSCRIPTION_ID if not set +if [[ -z "${ARM_SUBSCRIPTION_ID}" ]]; then + echo -e "${YELLOW}ARM_SUBSCRIPTION_ID not set, detecting from Azure CLI...${NC}" + ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null) + if [[ -z "${ARM_SUBSCRIPTION_ID}" ]]; then + echo -e "${RED}Error: Could not auto-detect ARM_SUBSCRIPTION_ID. Please run 'az login' or set ARM_SUBSCRIPTION_ID${NC}" + exit 1 + fi + echo -e "${GREEN}Detected subscription: ${ARM_SUBSCRIPTION_ID}${NC}" + export ARM_SUBSCRIPTION_ID +fi + +# Auto-detect CUSTOM_LOCATIONS_OID if not set (for Bicep tests) +if [[ -z "${CUSTOM_LOCATIONS_OID}" ]] && [[ "$DEPLOYMENT_TYPE" == "bicep" || "$DEPLOYMENT_TYPE" == "both" ]]; then + echo -e "${YELLOW}CUSTOM_LOCATIONS_OID not set, detecting from Azure AD...${NC}" + CUSTOM_LOCATIONS_OID=$(az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv 2>/dev/null) + if [[ -z "${CUSTOM_LOCATIONS_OID}" ]]; then + echo -e "${RED}Error: Could not auto-detect CUSTOM_LOCATIONS_OID. Please ensure you have permissions to query Azure AD${NC}" + exit 1 + fi + echo -e "${GREEN}Detected Custom Locations OID: ${CUSTOM_LOCATIONS_OID}${NC}" + export CUSTOM_LOCATIONS_OID +fi + +# Generate strong admin password if not provided (for Bicep tests) +if [[ -z "${ADMIN_PASSWORD}" ]] && [[ "$DEPLOYMENT_TYPE" == "bicep" || "$DEPLOYMENT_TYPE" == "both" ]]; then + echo -e "${YELLOW}ADMIN_PASSWORD not set, generating strong password...${NC}" + ADMIN_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-24) + # Ensure password meets Azure complexity requirements (uppercase, lowercase, digit, special char) + ADMIN_PASSWORD="Aa1!${ADMIN_PASSWORD}" + echo -e "${GREEN}Generated admin password (save this): ${ADMIN_PASSWORD}${NC}" + export ADMIN_PASSWORD +fi + +echo -e "${GREEN}=== Deployment Tests ===${NC}" +# Set default test configuration values +export TEST_ENVIRONMENT="${TEST_ENVIRONMENT:-dev}" +export TEST_LOCATION="${TEST_LOCATION:-eastus2}" +export TEST_RESOURCE_PREFIX="${TEST_RESOURCE_PREFIX:-t$RANDOM}" +export TEST_INSTANCE="${TEST_INSTANCE:-001}" + +echo "Deployment Type: $DEPLOYMENT_TYPE" +echo "ARM Subscription: ${ARM_SUBSCRIPTION_ID}" +echo "Resource Prefix: ${TEST_RESOURCE_PREFIX}" +echo "Location: ${TEST_LOCATION}" +echo "" + +run_terraform_tests() { + export TEST_RESOURCE_GROUP_NAME="${TEST_RESOURCE_GROUP_NAME_PREFIX:-test-}terraform" + echo "Resource Group: ${TEST_RESOURCE_GROUP_NAME}" + + echo -e "${YELLOW}Running Terraform deployment tests...${NC}" + if go test $VERBOSE_FLAG -run TestTerraformFullSingleNodeClusterDeploy -timeout 2h; then + echo -e "${GREEN}โœ“ Terraform tests passed${NC}" + return 0 + else + echo -e "${RED}โœ— Terraform tests failed${NC}" + return 1 + fi +} + +run_bicep_tests() { + export TEST_RESOURCE_GROUP_NAME="${TEST_RESOURCE_GROUP_NAME_PREFIX:-test-}bicep" + echo "Resource Group: ${TEST_RESOURCE_GROUP_NAME}" + + echo -e "${YELLOW}Running Bicep deployment tests...${NC}" + if go test $VERBOSE_FLAG -run TestBicepFullSingleNodeClusterDeploy -timeout 2h; then + echo -e "${GREEN}โœ“ Bicep tests passed${NC}" + return 0 + else + echo -e "${RED}โœ— Bicep tests failed${NC}" + return 1 + fi +} + +# Run tests based on deployment type +EXIT_CODE=0 + +case $DEPLOYMENT_TYPE in + terraform) + run_terraform_tests || EXIT_CODE=$? + ;; + bicep) + run_bicep_tests || EXIT_CODE=$? + ;; + both) + run_terraform_tests || EXIT_CODE=$? + echo "" + run_bicep_tests || EXIT_CODE=$? + ;; +esac + +echo "" +if [[ $EXIT_CODE -eq 0 ]]; then + echo -e "${GREEN}=== All tests completed successfully ===${NC}" +else + echo -e "${RED}=== Some tests failed ===${NC}" +fi + +exit $EXIT_CODE diff --git a/blueprints/full-single-node-cluster/tests/setup.go b/blueprints/full-single-node-cluster/tests/setup.go new file mode 100644 index 00000000..fd55908c --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/setup.go @@ -0,0 +1,147 @@ +// Package test provides validation and testing utilities for the full-single-node-cluster blueprint. +// This file contains post-deployment setup functions for permissions and connectivity. +package test + +import ( + "context" + "fmt" + "os/exec" + "strings" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2" + "github.com/google/uuid" + "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/gruntwork-io/terratest/modules/retry" + msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// setupEventHubPermissions assigns Event Hub Data Receiver role to the current user. +// This enables Event Hub message consumption in validation tests. +// Skips gracefully if Event Hub is not deployed or if permissions cannot be determined. +func setupEventHubPermissions(t *testing.T, outputs *BlueprintOutputs, subscriptionID, resourceGroupName string) { + t.Helper() + + // Get Event Hub namespace name from outputs + eventhubNamespace, ok := outputs.Messaging["eventhub_namespace_name"].(string) + if !ok || eventhubNamespace == "" || eventhubNamespace == "Not deployed" { + t.Log("Event Hub namespace not deployed, skipping role assignment") + return + } + + t.Logf("Configuring Event Hub permissions for namespace: %s", eventhubNamespace) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + // Create Azure credential + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + // Create Microsoft Graph client to get current user's principal ID + graphClient, err := msgraphsdk.NewGraphServiceClientWithCredentials(cred, []string{"https://graph.microsoft.com/.default"}) + if err != nil { + t.Logf("Failed to create Microsoft Graph client: %v", err) + t.Log("โš ๏ธ Skipping Event Hub role assignment") + return + } + + // Get current signed-in user + user, err := graphClient.Me().Get(ctx, nil) + if err != nil { + t.Logf("Failed to get signed-in user from Microsoft Graph: %v", err) + t.Log("โš ๏ธ Skipping Event Hub role assignment. Please ensure you're signed in with proper permissions") + return + } + + if user.GetId() == nil { + t.Log("โš ๏ธ Unable to get user principal ID, skipping role assignment") + return + } + + principalID := *user.GetId() + t.Logf("Current user principal ID: %s", principalID) + + // Azure Event Hubs Data Receiver role definition ID (built-in role, same across all tenants) + roleDefinitionID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/a638d3c7-ab3a-418d-83e6-5f17a39d4fde", subscriptionID) + + // Construct Event Hub namespace scope + scope := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.EventHub/namespaces/%s", + subscriptionID, resourceGroupName, eventhubNamespace) + + // Create role assignments client + roleClient, err := armauthorization.NewRoleAssignmentsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create role assignments client: %v", err) + } + + // Generate a unique name for the role assignment + roleAssignmentName := uuid.New().String() + + // Create role assignment parameters + params := armauthorization.RoleAssignmentCreateParameters{ + Properties: &armauthorization.RoleAssignmentProperties{ + PrincipalID: to.Ptr(principalID), + RoleDefinitionID: to.Ptr(roleDefinitionID), + PrincipalType: to.Ptr(armauthorization.PrincipalTypeUser), + }, + } + + t.Log("Creating role assignment...") + _, err = roleClient.Create(ctx, scope, roleAssignmentName, params, nil) + if err != nil { + // Role assignment might already exist, which is fine + if strings.Contains(err.Error(), "RoleAssignmentExists") { + t.Log("โœ“ Role assignment already exists") + } else { + t.Logf("โš ๏ธ Failed to create role assignment: %v", err) + t.Log("Tests may fail if Event Hub permissions are not configured") + } + return + } + + t.Log("โœ“ Successfully assigned 'Azure Event Hubs Data Receiver' role") +} + +// setupArcProxy establishes connectivity to the Arc-connected Kubernetes cluster. +// Starts 'az connectedk8s proxy' command and waits for connection to be ready. +// Returns proxy command (caller must terminate) and kubectl options configured for the proxy. +func setupArcProxy(t *testing.T, outputs *BlueprintOutputs) (*exec.Cmd, *k8s.KubectlOptions) { + t.Helper() + + clusterConnection := outputs.ClusterConnection + arcClusterName := clusterConnection["arc_cluster_name"].(string) + arcResourceGroup := clusterConnection["arc_cluster_resource_group"].(string) + + proxyCmd := exec.Command("az", + "connectedk8s", + "proxy", + "--name", arcClusterName, + "--resource-group", arcResourceGroup, + ) + + err := proxyCmd.Start() + require.NoError(t, err, "Failed to start Arc proxy") + + t.Logf("Arc proxy started (PID: %d)", proxyCmd.Process.Pid) + + t.Logf("Waiting for Arc proxy to establish connection...") + kubectlOptions := k8s.NewKubectlOptions("", "", "") + retry.DoWithRetry(t, "Wait for proxy", 6, 5*time.Second, func() (string, error) { + _, err := k8s.ListNamespacesE(t, kubectlOptions, metav1.ListOptions{}) + if err != nil { + return "", fmt.Errorf("proxy not ready yet: %v", err) + } + return "Proxy is ready", nil + }) + + t.Log("โœ“ Arc proxy connection established") + return proxyCmd, kubectlOptions +} diff --git a/blueprints/full-single-node-cluster/tests/validation.go b/blueprints/full-single-node-cluster/tests/validation.go new file mode 100644 index 00000000..f2a9a69b --- /dev/null +++ b/blueprints/full-single-node-cluster/tests/validation.go @@ -0,0 +1,379 @@ +// Package test provides validation and testing utilities for the full-single-node-cluster blueprint. +// This file contains shared validation logic used by both Terraform and Bicep deployment tests. +package test + +import ( + "context" + "encoding/json" + "fmt" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs" + "github.com/gruntwork-io/terratest/modules/azure" + "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/gruntwork-io/terratest/modules/retry" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeploymentContext holds the parsed outputs and connection resources for a deployed blueprint. +// Used to pass deployment state between setup and validation phases. +type DeploymentContext struct { + Outputs *BlueprintOutputs + ProxyCmd *exec.Cmd + KubectlOptions *k8s.KubectlOptions +} + +// setupPermissionsAndProxy configures post-deployment permissions (Event Hub access) and establishes Arc proxy connection. +// Returns proxy command for cleanup and kubectl options for cluster access. +func setupPermissionsAndProxy(t *testing.T, outputs *BlueprintOutputs, subscriptionID, resourceGroupName string) (*exec.Cmd, *k8s.KubectlOptions) { + setupEventHubPermissions(t, outputs, subscriptionID, resourceGroupName) + return setupArcProxy(t, outputs) +} + +// prepareTerraformOutputsAndSetup converts raw deployment outputs to strongly-typed BlueprintOutputs, +// configures post-deployment permissions (Event Hub access), and establishes Arc proxy connection. +func prepareTerraformOutputsAndSetup(t *testing.T, rawOutputs map[string]any, subscriptionID, resourceGroupName string) *DeploymentContext { + outputs := ParseTerraformBlueprintOutputs(t, rawOutputs) + proxyCmd, kubectlOptions := setupPermissionsAndProxy(t, outputs, subscriptionID, resourceGroupName) + return &DeploymentContext{Outputs: outputs, ProxyCmd: proxyCmd, KubectlOptions: kubectlOptions} +} + +// prepareBicepOutputsAndSetup converts raw Bicep deployment outputs to strongly-typed BlueprintOutputs, +// configures post-deployment permissions (Event Hub access), and establishes Arc proxy connection. +// Bicep outputs use camelCase while struct tags use snake_case. +func prepareBicepOutputsAndSetup(t *testing.T, rawOutputs map[string]any, subscriptionID, resourceGroupName string) *DeploymentContext { + outputs := ParseBicepBlueprintOutputs(t, rawOutputs) + proxyCmd, kubectlOptions := setupPermissionsAndProxy(t, outputs, subscriptionID, resourceGroupName) + return &DeploymentContext{Outputs: outputs, ProxyCmd: proxyCmd, KubectlOptions: kubectlOptions} +} + +// validateDeploymentWithoutMessaging runs validation tests for deployed infrastructure. +func validateDeploymentWithoutMessaging(t *testing.T, ctx *DeploymentContext, subscriptionID, resourceGroupName, resourcePrefix, location string) { + runValidationTests(t, ctx.Outputs, subscriptionID, resourceGroupName, resourcePrefix, location, ctx.ProxyCmd, ctx.KubectlOptions) +} + +// validateDeploymentWithMessaging runs validation tests plus Event Hub message consumption validation. +// Event Hub validation is Terraform-specific due to additional configuration requirements. +func validateDeploymentWithMessaging(t *testing.T, ctx *DeploymentContext, subscriptionID, resourceGroupName, resourcePrefix, location string) { + runValidationTests(t, ctx.Outputs, subscriptionID, resourceGroupName, resourcePrefix, location, ctx.ProxyCmd, ctx.KubectlOptions) + runMessagingValidation(t, ctx.Outputs) +} + +// runValidationTests executes the complete validation test suite for deployed infrastructure. +// Accepts strongly-typed BlueprintOutputs to enforce compile-time contract validation. +// Tests include: Azure resources, Kubernetes cluster, IoT Operations, and resource connectivity. +// Automatically cleans up Arc proxy connection on completion. +func runValidationTests(t *testing.T, outputs *BlueprintOutputs, subscriptionID, resourceGroupName, resourcePrefix, location string, proxyCmd *exec.Cmd, kubectlOptions *k8s.KubectlOptions) { + defer func() { + if proxyCmd != nil && proxyCmd.Process != nil { + t.Logf("Stopping Arc proxy (PID: %d)...", proxyCmd.Process.Pid) + _ = proxyCmd.Process.Signal(syscall.SIGTERM) + time.Sleep(1 * time.Second) + _ = proxyCmd.Process.Kill() + } + }() + // Validate deployment summary + t.Run("ValidateDeploymentSummary", func(t *testing.T) { + deploymentSummary := outputs.DeploymentSummary + require.NotNil(t, deploymentSummary, "deployment_summary output is nil") + + rgName := deploymentSummary["resource_group"].(string) + assert.NotEmpty(t, rgName, "Resource group name should be set") + assert.Equal(t, resourceGroupName, rgName, "Resource group should match input variable") + + // Use Azure module to verify resource group exists + assert.True(t, azure.ResourceGroupExists(t, rgName, subscriptionID), + "Resource group should exist in Azure") + }) + + // Validate security and identity resources + t.Run("ValidateSecurityIdentity", func(t *testing.T) { + securityIdentity := outputs.SecurityIdentity + + keyVaultName := securityIdentity["key_vault_name"].(string) + assert.NotEmpty(t, keyVaultName, "Key Vault name should be set") + assert.NotEmpty(t, securityIdentity["key_vault_uri"], "Key Vault URI should be set") + assert.NotEmpty(t, securityIdentity["aio_identity"], "AIO identity should be set") + assert.Contains(t, securityIdentity["key_vault_uri"], "https://", "Key Vault URI should be HTTPS") + + t.Logf("Verifying Key Vault %s exists in resource group %s", keyVaultName, resourceGroupName) + }) + + // Validate observability resources + t.Run("ValidateObservability", func(t *testing.T) { + observability := outputs.Observability + + logAnalyticsName := observability["log_analytics_workspace_name"].(string) + assert.NotEmpty(t, logAnalyticsName, "Log Analytics workspace name should be set") + assert.NotEmpty(t, observability["azure_monitor_workspace_name"], "Azure Monitor workspace name should be set") + assert.NotEmpty(t, observability["grafana_name"], "Grafana name should be set") + assert.NotEmpty(t, observability["grafana_endpoint"], "Grafana endpoint should be set") + assert.Contains(t, observability["grafana_endpoint"], "https://", "Grafana endpoint should be HTTPS") + + // Use Azure module to verify Log Analytics workspace exists + assert.True(t, azure.LogAnalyticsWorkspaceExists(t, logAnalyticsName, resourceGroupName, subscriptionID), + "Log Analytics workspace should exist in Azure") + }) + + // Validate networking resources + t.Run("ValidateNetworking", func(t *testing.T) { + natGateway := outputs.NatGateway + natGatewayIPs := outputs.NatGatewayPublicIps + + if natGateway != nil { + natGatewayMap := natGateway.(map[string]any) + assert.NotEmpty(t, natGatewayMap["name"], "NAT gateway name should be set") + assert.NotEmpty(t, natGatewayMap["id"], "NAT gateway ID should be set") + } + + if natGatewayIPs != nil { + assert.NotNil(t, natGatewayIPs, "NAT gateway public IPs should be configured") + } + }) + + // Validate data storage outputs + t.Run("ValidateDataStorage", func(t *testing.T) { + dataStorage := outputs.DataStorage + + storageAccountName := dataStorage["storage_account_name"].(string) + assert.NotEmpty(t, storageAccountName, "Storage account name should be set") + + // Use Azure module to verify storage account exists + assert.True(t, azure.StorageAccountExists(t, storageAccountName, resourceGroupName, subscriptionID), + "Storage account should exist in Azure") + }) + + // Validate container registry outputs + t.Run("ValidateContainerRegistry", func(t *testing.T) { + acr := outputs.ContainerRegistry + + acrName := acr["name"].(string) + assert.NotEmpty(t, acrName, "ACR name should be set") + assert.NotEmpty(t, acr["id"], "ACR ID should be set") + assert.Contains(t, acrName, resourcePrefix, "ACR name should contain resource prefix") + + // Use Azure module to verify container registry exists + assert.True(t, azure.ContainerRegistryExists(t, acrName, resourceGroupName, subscriptionID), + "Container registry should exist in Azure") + }) + + // Validate messaging resources + t.Run("ValidateMessaging", func(t *testing.T) { + messaging := outputs.Messaging + + assert.NotEmpty(t, messaging["event_grid_topic_name"], "Event Grid topic name should be set") + assert.NotEmpty(t, messaging["event_grid_topic_endpoint"], "Event Grid endpoint should be set") + assert.NotEmpty(t, messaging["eventhub_namespace_name"], "Event Hub namespace name should be set") + assert.NotEmpty(t, messaging["eventhub_name"], "Event Hub name should be set") + + eventGridEndpoint := messaging["event_grid_topic_endpoint"].(string) + assert.True(t, strings.Contains(eventGridEndpoint, "eventgrid.azure.net"), + "Event Grid endpoint should be valid Azure Event Grid endpoint") + }) + + // Validate VM host resources + t.Run("ValidateVMHost", func(t *testing.T) { + vmHost := outputs.VmHost + + require.NotNil(t, vmHost, "VM host output should exist") + vmHosts := vmHost.([]any) + assert.NotEmpty(t, vmHosts, "At least one VM host should be deployed") + + for i, host := range vmHosts { + hostMap := host.(map[string]any) + vmName := hostMap["name"].(string) + assert.NotEmpty(t, vmName, fmt.Sprintf("VM %d name should be set", i)) + + assert.True(t, azure.VirtualMachineExists(t, vmName, resourceGroupName, subscriptionID), + fmt.Sprintf("VM %s should exist in Azure", vmName)) + } + }) + + // Validate Arc connected cluster + t.Run("ValidateArcConnectedCluster", func(t *testing.T) { + arcCluster := outputs.ArcConnectedCluster + + assert.NotEmpty(t, arcCluster["name"], "Arc cluster name should be set") + assert.NotEmpty(t, arcCluster["location"], "Arc cluster location should be set") + assert.Equal(t, location, arcCluster["location"], "Arc cluster location should match input") + }) + + // Validate cluster connection outputs + t.Run("ValidateClusterConnection", func(t *testing.T) { + clusterConnection := outputs.ClusterConnection + + assert.NotEmpty(t, clusterConnection["arc_cluster_name"], "Arc cluster name should be set") + assert.NotEmpty(t, clusterConnection["arc_cluster_resource_group"], "Arc resource group should be set") + assert.NotEmpty(t, clusterConnection["arc_proxy_command"], "Arc proxy command should be set") + assert.Contains(t, clusterConnection["arc_cluster_name"], resourcePrefix, "Cluster name should contain resource prefix") + }) + + // Validate Azure IoT Operations outputs + t.Run("ValidateAzureIoTOperations", func(t *testing.T) { + aio := outputs.AzureIotOperations + + assert.NotEmpty(t, aio["instance_name"], "AIO instance name should be set") + assert.NotEmpty(t, aio["custom_location_id"], "Custom location ID should be set") + assert.NotEmpty(t, aio["namespace"], "AIO namespace should be set") + }) + + // Validate assets outputs + t.Run("ValidateAssets", func(t *testing.T) { + assets := outputs.Assets + + require.NotNil(t, assets["assets"], "Assets list should exist") + require.NotNil(t, assets["asset_endpoint_profiles"], "Asset endpoint profiles should exist") + }) + + // Validate ACR network posture + t.Run("ValidateACRNetworkPosture", func(t *testing.T) { + acrNetworkPosture := outputs.ACRNetworkPosture + require.NotNil(t, acrNetworkPosture, "ACR network posture output should exist") + }) + + // Validate Kubernetes cluster connectivity + t.Run("ValidateKubernetesCluster", func(t *testing.T) { + t.Run("CheckClusterNodes", func(t *testing.T) { + nodes := k8s.GetNodes(t, kubectlOptions) + require.NotEmpty(t, nodes, "Cluster should have at least one node") + + for _, node := range nodes { + t.Logf("Node: %s, Status: %v", node.Name, node.Status.Conditions) + isReady := false + for _, condition := range node.Status.Conditions { + if condition.Type == "Ready" && condition.Status == "True" { + isReady = true + break + } + } + assert.True(t, isReady, fmt.Sprintf("Node %s should be in Ready state", node.Name)) + } + }) + + t.Run("CheckAzureIoTOperations", func(t *testing.T) { + aio := outputs.AzureIotOperations + namespace := aio["namespace"].(string) + + retry.DoWithRetry(t, "Check AIO namespace exists", 3, 10*time.Second, func() (string, error) { + _, err := k8s.GetNamespaceE(t, kubectlOptions, namespace) + if err != nil { + return "", fmt.Errorf("namespace %s not found: %v", namespace, err) + } + return "Namespace found", nil + }) + + kubectlOptionsAIO := k8s.NewKubectlOptions("", "", namespace) + pods := k8s.ListPods(t, kubectlOptionsAIO, metav1.ListOptions{}) + require.NotEmpty(t, pods, fmt.Sprintf("AIO namespace %s should have running pods", namespace)) + + runningCount := 0 + for _, pod := range pods { + if pod.Status.Phase == corev1.PodRunning { + runningCount++ + } + } + assert.Greater(t, runningCount, 0, "At least one pod should be in Running state") + }) + + t.Run("CheckBasicResources", func(t *testing.T) { + services := k8s.ListServices(t, kubectlOptions, metav1.ListOptions{}) + assert.NotEmpty(t, services, "Cluster should have services") + + kubectlOptionsSystem := k8s.NewKubectlOptions("", "", "kube-system") + systemPods := k8s.ListPods(t, kubectlOptionsSystem, metav1.ListOptions{}) + assert.NotEmpty(t, systemPods, "kube-system namespace should have pods") + }) + }) +} + +// runMessagingValidation validates Event Hub message consumption and processing. +// Terraform-specific test that verifies messages can be received and parsed. +// Requires Event Hub Data Receiver role assignment (configured by setupEventHubPermissions). +func runMessagingValidation(t *testing.T, outputs *BlueprintOutputs) { + t.Run("CheckMQTTBroker", func(t *testing.T) { + aio := outputs.AzureIotOperations + namespace := aio["namespace"].(string) + + kubectlOptionsAIO := k8s.NewKubectlOptions("", "", namespace) + services := k8s.ListServices(t, kubectlOptionsAIO, metav1.ListOptions{LabelSelector: "app.kubernetes.io/name=microsoft-iotoperations-mqttbroker"}) + require.NotEmpty(t, services, "MQTT Broker service should exist in AIO namespace") + }) + + t.Run("ValidateEventHubsMessages", func(t *testing.T) { + messaging := outputs.Messaging + eventhubNamespace := messaging["eventhub_namespace_name"].(string) + eventhubName := messaging["eventhub_name"].(string) + + t.Logf("Connecting to Event Hub: %s/%s", eventhubNamespace, eventhubName) + + cred, err := azidentity.NewDefaultAzureCredential(nil) + require.NoError(t, err, "Failed to create Azure credential") + + consumerClient, err := azeventhubs.NewConsumerClient( + eventhubNamespace+".servicebus.windows.net", + eventhubName, + azeventhubs.DefaultConsumerGroup, + cred, + nil, + ) + require.NoError(t, err, "Failed to create Event Hub consumer client") + defer consumerClient.Close(context.Background()) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + props, err := consumerClient.GetEventHubProperties(ctx, nil) + require.NoError(t, err, "Failed to get Event Hub properties") + + t.Logf("Event Hub has %d partitions", len(props.PartitionIDs)) + require.NotEmpty(t, props.PartitionIDs, "Event Hub should have at least one partition") + + partitionID := props.PartitionIDs[0] + t.Logf("Consuming from partition: %s", partitionID) + + partitionClient, err := consumerClient.NewPartitionClient(partitionID, &azeventhubs.PartitionClientOptions{ + StartPosition: azeventhubs.StartPosition{ + Earliest: to.Ptr(true), + }, + }) + require.NoError(t, err, "Failed to create partition client") + defer partitionClient.Close(context.Background()) + + receiveCtx, receiveCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer receiveCancel() + + events, err := partitionClient.ReceiveEvents(receiveCtx, 5, nil) + if err != nil && err != context.DeadlineExceeded { + require.NoError(t, err, "Failed to receive events from Event Hub") + } + + require.NotEmpty(t, events, "Should receive at least one message from Event Hub") + t.Logf("Received %d messages from Event Hub", len(events)) + + for i, event := range events { + t.Logf("Message %d: %d bytes", i, len(event.Body)) + assert.NotNil(t, event.Body, "Message body should not be nil") + + var jsonData map[string]any + if err := json.Unmarshal(event.Body, &jsonData); err == nil { + t.Logf("Message %d is valid JSON with %d fields", i, len(jsonData)) + assert.NotEmpty(t, jsonData, "JSON message should have fields") + } else { + t.Logf("Message %d is not JSON: %s", i, string(event.Body)) + } + + assert.GreaterOrEqual(t, event.SequenceNumber, int64(0), "Sequence number should be non-negative") + assert.NotNil(t, event.EnqueuedTime, "Enqueued time should be set") + } + }) +} diff --git a/blueprints/minimum-single-node-cluster/bicep/README.md b/blueprints/minimum-single-node-cluster/bicep/README.md index d8716323..1fde4024 100644 --- a/blueprints/minimum-single-node-cluster/bicep/README.md +++ b/blueprints/minimum-single-node-cluster/bicep/README.md @@ -7,21 +7,21 @@ Deploys the minimal set of resources required for Azure IoT Operations on a sing ## Parameters -| Name | Description | Type | Default | Required | -|:------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:---------| -| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | -| resourceGroupName | The name for the resource group. If not provided, a default name will be generated. | `string` | [format('rg-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | -| adminPassword | Password used for the host VM. | `securestring` | n/a | yes | -| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
Can be retrieved using:

  az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes | -| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | -| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | -| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | -| namespacedDevices | List of namespaced devices to create. | `array` | [] | no | -| assetEndpointProfiles | List of asset endpoint profiles to create. | `array` | [] | no | -| legacyAssets | List of legacy assets to create. | `array` | [] | no | -| namespacedAssets | List of namespaced assets to create. | `array` | [] | no | -| shouldCreateDefaultNamespacedAsset | Whether to create a default namespaced asset and device. | `bool` | `true` | no | +| Name | Description | Type | Default | Required | +|:------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:---------| +| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | +| resourceGroupName | The name for the resource group. If not provided, a default name will be generated. | `string` | [format('rg-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | +| adminPassword | Password used for the host VM. | `securestring` | n/a | yes | +| customLocationsOid | The object id of the Custom Locations Entra ID application for your tenant.
Can be retrieved using:

  az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv
| `string` | n/a | yes | +| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | +| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | +| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | +| namespacedDevices | List of namespaced devices to create. | `[_1.NamespacedDevice](#user-defined-types)[]` | [] | no | +| assetEndpointProfiles | List of asset endpoint profiles to create. | `[_1.AssetEndpointProfile](#user-defined-types)[]` | [] | no | +| legacyAssets | List of legacy assets to create. | `[_1.LegacyAsset](#user-defined-types)[]` | [] | no | +| namespacedAssets | List of namespaced assets to create. | `[_1.NamespacedAsset](#user-defined-types)[]` | [] | no | +| shouldCreateDefaultNamespacedAsset | Whether to create a default namespaced asset and device. | `bool` | `true` | no | ## Resources @@ -379,6 +379,7 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | | shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | | customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no | +| registryEndpoints | List of additional container registry endpoints for pulling custom artifacts. MCR is always added automatically. | `array` | [] | no | | akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no | | customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | | additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `array` | [] | no | @@ -409,27 +410,29 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | postInitScripts | `Microsoft.Resources/deployments` | 2025-04-01 | | iotOpsInstance | `Microsoft.Resources/deployments` | 2025-04-01 | | akriConnectors | `Microsoft.Resources/deployments` | 2025-04-01 | +| registryEndpointsModule | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScriptsSecrets | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScripts | `Microsoft.Resources/deployments` | 2025-04-01 | #### Outputs for edgeIotOps -| Name | Type | Description | -|:---------------------------|:---------|:-------------------------------------------------------------------| -| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | -| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | -| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | -| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | -| customLocationId | `string` | The ID of the deployed Custom Location. | -| customLocationName | `string` | The name of the deployed Custom Location. | -| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | -| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | -| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | -| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | -| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | -| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | +| Name | Type | Description | +|:---------------------------|:---------|:----------------------------------------------------------------------| +| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | +| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | +| aioNamespace | `string` | The namespace in the cluster where Azure IoT Operations is installed. | +| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | +| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | +| customLocationId | `string` | The ID of the deployed Custom Location. | +| customLocationName | `string` | The name of the deployed Custom Location. | +| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | +| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | +| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | +| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | +| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | +| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | ### edgeAssets diff --git a/blueprints/only-cloud-single-node-cluster/bicep/README.md b/blueprints/only-cloud-single-node-cluster/bicep/README.md index fb326625..5a8f74f3 100644 --- a/blueprints/only-cloud-single-node-cluster/bicep/README.md +++ b/blueprints/only-cloud-single-node-cluster/bicep/README.md @@ -179,6 +179,7 @@ Deploys Azure observability resources including Azure Monitor Workspace, Log Ana | logAnalyticsName | `string` | The Log Analytics Workspace name. | | logAnalyticsId | `string` | The Log Analytics Workspace ID. | | grafanaName | `string` | The Azure Managed Grafana name. | +| grafanaEndpoint | `string` | The Azure Managed Grafana endpoint. | | metricsDataCollectionRuleName | `string` | The metrics data collection rule name. | | logsDataCollectionRuleName | `string` | The logs data collection rule name. | | monitorPrivateLinkScopeId | `string` | Azure Monitor Private Link Scope resource ID. | diff --git a/blueprints/only-edge-iot-ops/bicep/README.md b/blueprints/only-edge-iot-ops/bicep/README.md index f0fa18bc..a0da84b6 100644 --- a/blueprints/only-edge-iot-ops/bicep/README.md +++ b/blueprints/only-edge-iot-ops/bicep/README.md @@ -7,35 +7,35 @@ Deploys Azure IoT Operations on an existing Arc-enabled Kubernetes cluster witho ## Parameters -| Name | Description | Type | Default | Required | -|:------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|:---------| -| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | -| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | -| customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | -| sseIdentityName | The name of the User Assigned Managed Identity for Secret Sync Extension. | `string` | [format('id-{0}-sse-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| sseKeyVaultName | The name of the Key Vault for Secret Sync Extension. Required when providing sseIdentityName. | `string` | [format('kv-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| sseKeyVaultResourceGroupName | The name of the Resource Group for the Key Vault for Secret Sync Extension. Required when providing sseIdentityName. | `string` | [resourceGroup().name] | no | -| shouldAssignSseKeyVaultRoles | Whether to assign roles for Key Vault to the provided Secret Sync Identity. | `bool` | `true` | no | -| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | [parameters('sseKeyVaultName')] | no | -| deployIdentityName | The resource name for a managed identity that will be given deployment admin permissions. | `string` | [format('id-{0}-deploy-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [parameters('sseKeyVaultResourceGroupName')] | no | -| deployUserTokenSecretName | The name of the secret in Key Vault that has the token for the deploy user with cluster-admin role. | `string` | n/a | no | -| deploymentScriptsSecretNamePrefix | The prefix used with constructing the secret name that will have the deployment script. | `string` | [format('{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| shouldAssignDeployIdentityRoles | Whether to assign roles to the deploy identity. | `bool` | `true` | no | -| shouldInitAio | Whether to init Azure IoT Operations. (For debugging) | `bool` | `true` | no | -| aioIdentityName | The name of the User Assigned Managed Identity for Azure IoT Operations. | `string` | [format('id-{0}-aio-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| aioInstanceName | The name for the Azure IoT Operations Instance resource. | `string` | [format('{0}-ops-instance', parameters('arcConnectedClusterName'))] | no | -| arcConnectedClusterName | The resource name for the Arc-enabled Kubernetes cluster. | `string` | [format('arck-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| schemaRegistryName | The resource name for the Azure Data Registry Schema Registry for Azure IoT Operations. | `string` | [format('sr-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| adrNamespaceName | The resource name for the ADR Namespace for Azure IoT Operations. Optional parameter for referencing an existing ADR namespace. | `string` | n/a | no | -| shouldDeployAio | Whether to deploy Azure IoT Operations. (For debugging) | `bool` | `true` | no | -| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous Azure IoT Operations MQ Broker Listener. Should only be used for dev or test environments. | `bool` | `false` | no | -| shouldDeployResourceSyncRules | Whether to deploy Custom Locations Resource Sync Rules for the Azure IoT Operations resources. | `bool` | `true` | no | -| namespacedDevices | List of namespaced devices to create. | `array` | [] | no | -| assetEndpointProfiles | List of asset endpoint profiles to create. | `array` | [] | no | -| legacyAssets | List of legacy assets to create. | `array` | [] | no | -| namespacedAssets | List of namespaced assets to create. | `array` | [] | no | -| shouldCreateDefaultNamespacedAsset | Whether to create a default namespaced asset and device. | `bool` | `true` | no | +| Name | Description | Type | Default | Required | +|:------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|:---------| +| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | +| telemetry_opt_out | Whether to opt-out of telemetry. Set to true to disable telemetry. | `bool` | `false` | no | +| customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | +| sseIdentityName | The name of the User Assigned Managed Identity for Secret Sync Extension. | `string` | [format('id-{0}-sse-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| sseKeyVaultName | The name of the Key Vault for Secret Sync Extension. Required when providing sseIdentityName. | `string` | [format('kv-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| sseKeyVaultResourceGroupName | The name of the Resource Group for the Key Vault for Secret Sync Extension. Required when providing sseIdentityName. | `string` | [resourceGroup().name] | no | +| shouldAssignSseKeyVaultRoles | Whether to assign roles for Key Vault to the provided Secret Sync Identity. | `bool` | `true` | no | +| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | [parameters('sseKeyVaultName')] | no | +| deployIdentityName | The resource name for a managed identity that will be given deployment admin permissions. | `string` | [format('id-{0}-deploy-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [parameters('sseKeyVaultResourceGroupName')] | no | +| deployUserTokenSecretName | The name of the secret in Key Vault that has the token for the deploy user with cluster-admin role. | `string` | n/a | no | +| deploymentScriptsSecretNamePrefix | The prefix used with constructing the secret name that will have the deployment script. | `string` | [format('{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| shouldAssignDeployIdentityRoles | Whether to assign roles to the deploy identity. | `bool` | `true` | no | +| shouldInitAio | Whether to init Azure IoT Operations. (For debugging) | `bool` | `true` | no | +| aioIdentityName | The name of the User Assigned Managed Identity for Azure IoT Operations. | `string` | [format('id-{0}-aio-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| aioInstanceName | The name for the Azure IoT Operations Instance resource. | `string` | [format('{0}-ops-instance', parameters('arcConnectedClusterName'))] | no | +| arcConnectedClusterName | The resource name for the Arc-enabled Kubernetes cluster. | `string` | [format('arck-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| schemaRegistryName | The resource name for the Azure Data Registry Schema Registry for Azure IoT Operations. | `string` | [format('sr-{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| adrNamespaceName | The resource name for the ADR Namespace for Azure IoT Operations. Optional parameter for referencing an existing ADR namespace. | `string` | n/a | no | +| shouldDeployAio | Whether to deploy Azure IoT Operations. (For debugging) | `bool` | `true` | no | +| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous Azure IoT Operations MQ Broker Listener. Should only be used for dev or test environments. | `bool` | `false` | no | +| shouldDeployResourceSyncRules | Whether to deploy Custom Locations Resource Sync Rules for the Azure IoT Operations resources. | `bool` | `true` | no | +| namespacedDevices | List of namespaced devices to create. | `[_1.NamespacedDevice](#user-defined-types)[]` | [] | no | +| assetEndpointProfiles | List of asset endpoint profiles to create. | `[_1.AssetEndpointProfile](#user-defined-types)[]` | [] | no | +| legacyAssets | List of legacy assets to create. | `[_1.LegacyAsset](#user-defined-types)[]` | [] | no | +| namespacedAssets | List of namespaced assets to create. | `[_1.NamespacedAsset](#user-defined-types)[]` | [] | no | +| shouldCreateDefaultNamespacedAsset | Whether to create a default namespaced asset and device. | `bool` | `true` | no | ## Resources @@ -115,6 +115,7 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | | shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | | customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no | +| registryEndpoints | List of additional container registry endpoints for pulling custom artifacts. MCR is always added automatically. | `array` | [] | no | | akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no | | customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | | additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `array` | [] | no | @@ -145,27 +146,29 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | postInitScripts | `Microsoft.Resources/deployments` | 2025-04-01 | | iotOpsInstance | `Microsoft.Resources/deployments` | 2025-04-01 | | akriConnectors | `Microsoft.Resources/deployments` | 2025-04-01 | +| registryEndpointsModule | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScriptsSecrets | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScripts | `Microsoft.Resources/deployments` | 2025-04-01 | #### Outputs for edgeIotOps -| Name | Type | Description | -|:---------------------------|:---------|:-------------------------------------------------------------------| -| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | -| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | -| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | -| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | -| customLocationId | `string` | The ID of the deployed Custom Location. | -| customLocationName | `string` | The name of the deployed Custom Location. | -| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | -| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | -| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | -| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | -| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | -| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | +| Name | Type | Description | +|:---------------------------|:---------|:----------------------------------------------------------------------| +| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | +| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | +| aioNamespace | `string` | The namespace in the cluster where Azure IoT Operations is installed. | +| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | +| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | +| customLocationId | `string` | The ID of the deployed Custom Location. | +| customLocationName | `string` | The name of the deployed Custom Location. | +| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | +| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | +| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | +| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | +| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | +| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | ### edgeAssets diff --git a/blueprints/only-output-cncf-cluster-script/bicep/README.md b/blueprints/only-output-cncf-cluster-script/bicep/README.md index 092383bc..a967f2b3 100644 --- a/blueprints/only-output-cncf-cluster-script/bicep/README.md +++ b/blueprints/only-output-cncf-cluster-script/bicep/README.md @@ -21,7 +21,7 @@ Generates scripts for Azure IoT Operations CNCF cluster creation without deployi | shouldAssignRoles | Whether to assign roles for Arc Onboarding. | `bool` | `true` | no | | clusterAdminOid | The Object ID that will be given cluster-admin permissions. | `string` | n/a | no | | clusterAdminUpn | The User Principal Name that will be given cluster-admin permissions. | `string` | n/a | no | -| clusterNodeVirtualMachineNames | The names of the VMs for the cluster nodes. (Only needed if wanting this blueprint to deploy the scripts) | `array` | n/a | no | +| clusterNodeVirtualMachineNames | The names of the VMs for the cluster nodes. (Only needed if wanting this blueprint to deploy the scripts) | `string[]` | n/a | no | | clusterServerIp | The IP address for the server for the cluster. (Needed for multi-node cluster) | `string` | n/a | no | | clusterServerHostMachineUsername | Username used for the host machines that will be given kube-config settings on setup. | `string` | [parameters('common').resourcePrefix] | no | | clusterServerVirtualMachineName | The name of the VM for the cluster server. (Only needed if wanting this blueprint to deploy the scripts) | `string` | n/a | no | diff --git a/docs/_parts/infrastructure-sidebar.md b/docs/_parts/infrastructure-sidebar.md index 9ace62eb..cdbde6d1 100644 --- a/docs/_parts/infrastructure-sidebar.md +++ b/docs/_parts/infrastructure-sidebar.md @@ -52,5 +52,6 @@ - [Video Tools](/src/900-tools-utilities/901-video-tools/README) - [Aio Sidekick](/src/900-tools-utilities/902-aio-sidekick/README) - [Multi Asset Deploy](/src/900-tools-utilities/903-multi-asset-deploy/README) + - [Test Utilities](/src/900-tools-utilities/904-test-utilities/README) - [Azure Resource Providers](/src/azure-resource-providers/README) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 22a1ac0c..d5726502 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -225,6 +225,7 @@ - [Fabric Rti](/blueprints/fabric-rti/README) - [Full Multi Node Cluster](/blueprints/full-multi-node-cluster/README) - [Full Single Node Cluster](/blueprints/full-single-node-cluster/README) + - [Tests](/blueprints/full-single-node-cluster/tests/README) - [Minimum Single Node Cluster](/blueprints/minimum-single-node-cluster/README) - [Only Cloud Single Node Cluster](/blueprints/only-cloud-single-node-cluster/README) - [Only Edge Iot Ops](/blueprints/only-edge-iot-ops/README) @@ -422,6 +423,7 @@ - [Test Assets](/src/900-tools-utilities/901-video-tools/test-assets/README) - [Aio Sidekick](/src/900-tools-utilities/902-aio-sidekick/README) - [Multi Asset Deploy](/src/900-tools-utilities/903-multi-asset-deploy/README) + - [Test Utilities](/src/900-tools-utilities/904-test-utilities/README) - [Azure Resource Providers](/src/azure-resource-providers/README) - [Dataflows Acsa Egmqtt Bidirectional](/src/starter-kit/dataflows-acsa-egmqtt-bidirectional/README) - [Resource Group](/src/000-cloud/000-resource-group/bicep/README) @@ -519,6 +521,7 @@ - [Iot Ops Init](/src/100-edge/110-iot-ops/terraform/modules/iot-ops-init/README) - [Iot Ops Instance](/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/README) - [Opc Ua Simulator](/src/100-edge/110-iot-ops/terraform/modules/opc-ua-simulator/README) + - [Registry Endpoints](/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/README) - [Role Assignment](/src/100-edge/110-iot-ops/terraform/modules/role-assignment/README) - [Self Signed Ca](/src/100-edge/110-iot-ops/terraform/modules/self-signed-ca/README) - [Assets](/src/100-edge/111-assets/terraform/README) diff --git a/docs/contributing/testing-validation.md b/docs/contributing/testing-validation.md index 92200e11..b310bb38 100644 --- a/docs/contributing/testing-validation.md +++ b/docs/contributing/testing-validation.md @@ -302,9 +302,108 @@ enable_monitoring = true ## Blueprint Testing -### Integration Testing +Some blueprints include comprehensive test suites using Go and the Terratest framework. The testing infrastructure validates both IaC declarations and actual deployments. -Test blueprint deployments end-to-end: +### Blueprint Test Architecture + +**Shared Test Utilities:** [src/900-tools-utilities/904-test-utilities/](../../src/900-tools-utilities/904-test-utilities/) + +Provides reusable testing functions for all blueprints including: + +- Contract validation functions for Terraform and Bicep +- Deployment and cleanup utilities +- Output normalization across frameworks + +**Reference Implementation:** [blueprints/full-single-node-cluster/tests/](../../blueprints/full-single-node-cluster/tests/) + +Complete test suite demonstrating: + +- Contract tests for both Terraform and Bicep +- End-to-end deployment validation +- Helper scripts for test execution +- Output contract definitions + +### Contract Testing + +**Purpose:** Fast static validation ensuring output declarations match test expectations + +**Characteristics:** + +- Runs in seconds without Azure authentication +- Zero cost - no Azure resources created +- Validates IaC configuration correctness +- Catches drift before expensive deployments + +**Running Contract Tests:** + +```bash +cd blueprints/full-single-node-cluster/tests + +# Test both frameworks +./run-contract-tests.sh both + +# Test specific framework +./run-contract-tests.sh terraform +./run-contract-tests.sh bicep + +# Direct Go execution +go test -v -run Contract +``` + +### Deployment Testing + +**Purpose:** Full end-to-end validation with real Azure resource deployment + +**Characteristics:** + +- Creates billable Azure resources +- Tests actual infrastructure deployment +- Validates resource connectivity and functionality +- Duration: 30-45 minutes per test + +**Running Deployment Tests:** + +```bash +cd blueprints/full-single-node-cluster/tests + +# Enable automatic cleanup +export CLEANUP_RESOURCES=true + +# Test specific framework +./run-deployment-tests.sh terraform +./run-deployment-tests.sh bicep + +# Direct Go execution +go test -v -run TestTerraformFullSingleNodeClusterDeploy -timeout 2h +go test -v -run TestBicepFullSingleNodeClusterDeploy -timeout 2h +``` + +**Environment Variables:** + +- `CLEANUP_RESOURCES` - Auto-delete resources after test (default: `false`) +- `TEST_ENVIRONMENT` - Environment name (default: `dev`) +- `TEST_LOCATION` - Azure region (default: `eastus2`) +- `TEST_RESOURCE_PREFIX` - Resource naming prefix (default: `t6`) +- `SKIP_BICEP_DEPLOYMENT` - Use existing deployment (default: `false`) + +### Blueprint Test Organization + +Each blueprint test suite includes: + +```text +blueprints/{blueprint-name}/tests/ +โ”œโ”€โ”€ outputs.go # Output contract definition +โ”œโ”€โ”€ contract_terraform_test.go # Terraform contract validation +โ”œโ”€โ”€ contract_bicep_test.go # Bicep contract validation +โ”œโ”€โ”€ deploy_terraform_test.go # Terraform deployment test +โ”œโ”€โ”€ deploy_bicep_test.go # Bicep deployment test +โ”œโ”€โ”€ validation.go # Shared validation functions +โ”œโ”€โ”€ setup.go # Post-deployment setup +โ”œโ”€โ”€ run-contract-tests.sh # Contract test runner +โ””โ”€โ”€ run-deployment-tests.sh # Deployment test runner +``` + +### Blueprint Integration Testing ```bash # Navigate to blueprint directory @@ -319,13 +418,25 @@ terraform plan -var-file="test.tfvars" # Apply to test environment terraform apply -var-file="test.tfvars" -auto-approve -# Validate deployment -./scripts/validate-deployment.sh # Clean up terraform destroy -var-file="test.tfvars" -auto-approve ``` +### Creating Blueprint Tests + +When creating a new blueprint, add comprehensive test coverage: + +1. **Define output contract** in `tests/outputs.go` with struct tags for both frameworks +2. **Create contract tests** for static validation +3. **Create deployment tests** for end-to-end validation +4. **Add helper scripts** for simplified test execution +5. **Document test requirements** in blueprint README + +**See:** [Blueprint Developer Guide](../getting-started/blueprint-developer.md#testing-and-validation) for detailed instructions + +**See:** [test-utilities README](../../src/900-tools-utilities/904-test-utilities/README.md) for complete API reference + ### Blueprint Validation Script Create comprehensive validation: @@ -522,13 +633,17 @@ go test -v -cpuprofile=cpu.prof -memprofile=mem.prof ./tests/... - **Use descriptive test names** that explain what is being tested - **Include both positive and negative test cases** - **Test error conditions** and edge cases +- **Implement contract tests** for fast validation before deployment tests +- **Use test-utilities package** for consistent testing patterns across blueprints ### Best Practices for Test Data - **Use parameterized tests** for multiple scenarios -- **Clean up test resources** automatically +- **Clean up test resources** automatically (set `CLEANUP_RESOURCES=true`) - **Isolate test environments** to prevent interference - **Use realistic test data** that represents production scenarios +- **Run contract tests first** to catch errors before expensive deployments +- **Enable cleanup in CI/CD** to prevent resource accumulation ### Validation Strategy diff --git a/docs/getting-started/blueprint-developer.md b/docs/getting-started/blueprint-developer.md index 1e8fe190..b620736d 100644 --- a/docs/getting-started/blueprint-developer.md +++ b/docs/getting-started/blueprint-developer.md @@ -295,6 +295,81 @@ Before coding, define: ## Testing and Validation +### Blueprint Test Infrastructure + +Selected blueprints include comprehensive test suites using Go and the Terratest framework. Tests validate both infrastructure declarations and actual deployments. + +**Test Types:** + +- **Contract Tests** - Fast static validation ensuring output declarations match test expectations (runs in seconds, zero Azure cost) +- **Deployment Tests** - Full end-to-end validation creating real Azure resources and testing functionality (30-45 minutes, creates billable resources) + +**Available Test Infrastructure:** + +- **Shared utilities**: [src/900-tools-utilities/904-test-utilities/](../../src/900-tools-utilities/904-test-utilities/) - Reusable test functions for all blueprints +- **Reference implementation**: [blueprints/full-single-node-cluster/tests/](../../blueprints/full-single-node-cluster/tests/) - Complete test suite example + +### Creating Tests for Your Blueprint + +1. **Create test directory structure**: + + ```bash + cd blueprints/my-custom-blueprint + mkdir tests + cd tests + ``` + +2. **Initialize Go module**: + + ```bash + go mod init github.com/microsoft/edge-ai/blueprints/my-custom-blueprint/tests + go get github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities + go get github.com/gruntwork-io/terratest/modules/terraform + ``` + +3. **Define output contract** in `tests/outputs.go`: + + Define a struct matching your blueprint's outputs with framework-specific tags. + + **See:** [full-single-node-cluster/tests/outputs.go](../../blueprints/full-single-node-cluster/tests/outputs.go) for pattern + +4. **Create contract tests**: + + Create `contract_terraform_test.go` and `contract_bicep_test.go` that validate declared outputs match your struct. + + **See:** [full-single-node-cluster/tests/contract_terraform_test.go](../../blueprints/full-single-node-cluster/tests/contract_terraform_test.go) + +5. **Create deployment tests**: + + Create `deploy_terraform_test.go` and `deploy_bicep_test.go` for end-to-end validation. + + **See:** [full-single-node-cluster/tests/deploy_terraform_test.go](../../blueprints/full-single-node-cluster/tests/deploy_terraform_test.go) + +6. **Add helper scripts**: + + Copy and adapt `run-contract-tests.sh` and `run-deployment-tests.sh` from the reference implementation. + +### Running Tests + +**Contract tests** (fast, run before every commit): + +```bash +cd blueprints/my-custom-blueprint/tests +./run-contract-tests.sh both +``` + +**Deployment tests** (slow, run before PR): + +```bash +# Set cleanup to auto-delete resources +export CLEANUP_RESOURCES=true + +# Run tests +./run-deployment-tests.sh terraform # or bicep, or both +``` + +**Documentation:** See [src/900-tools-utilities/904-test-utilities/README.md](../../src/900-tools-utilities/904-test-utilities/README.md) for complete testing guide and API reference + ### Linting and Code Quality Run validation tools before committing: @@ -353,35 +428,6 @@ npm run lint az group delete --name "rg-test-blueprint" --yes ``` -### Automated Testing - -Create test scripts for CI/CD validation: - -```bash -# Create test script -mkdir -p tests/blueprints/my-custom-blueprint -cat > tests/blueprints/my-custom-blueprint/test.sh << 'EOF' -#!/bin/bash -set -e - -echo "Testing my-custom-blueprint..." - -# Test Terraform -cd blueprints/my-custom-blueprint/terraform -terraform init -terraform validate -terraform plan -var-file="terraform.tfvars.example" - -# Test Bicep -cd ../bicep -az bicep build --file main.bicep - -echo "All tests passed!" -EOF - -chmod +x tests/blueprints/my-custom-blueprint/test.sh -``` - ## Best Practices ### Blueprint Integration Best Practices @@ -390,6 +436,16 @@ chmod +x tests/blueprints/my-custom-blueprint/test.sh 2. **Respect dependencies**: Use `depends_on` (Terraform) or `dependsOn` (Bicep) for proper ordering 3. **Maintain consistency**: Ensure Terraform and Bicep implementations produce equivalent results 4. **Follow naming conventions**: Use consistent resource naming across components +5. **Include test coverage**: Add contract and deployment tests for your blueprint +6. **Document test requirements**: Update README with test setup and usage instructions + +### Testing Best Practices + +1. **Run contract tests first**: Catch configuration errors before expensive deployments +2. **Use CLEANUP_RESOURCES**: Enable automatic cleanup during development to avoid resource accumulation +3. **Test both frameworks**: Ensure Terraform and Bicep produce equivalent results +4. **Update tests with changes**: Keep output contracts and validation logic synchronized with blueprint changes +5. **Validate before PR**: Run full deployment tests before submitting pull requests ### Documentation Standards diff --git a/scripts/generate-bicep-docs.py b/scripts/generate-bicep-docs.py index 3e895220..e7ec1d08 100755 --- a/scripts/generate-bicep-docs.py +++ b/scripts/generate-bicep-docs.py @@ -178,7 +178,19 @@ def extract_parameters(json_data: Dict[str, Any]) -> List[Dict[str, Any]]: ref_path = param_info["$ref"].split('/')[-1] param_data["type"] = f"[{ref_path}](#user-defined-types)" elif "type" in param_info: - param_data["type"] = param_info["type"] + param_type = param_info["type"] + # Check for typed arrays (array with items.$ref) + if param_type == "array" and "items" in param_info: + items = param_info["items"] + if "$ref" in items: + ref_path = items["$ref"].split('/')[-1] + param_data["type"] = f"[{ref_path}](#user-defined-types)[]" + elif "type" in items: + param_data["type"] = f"{items['type']}[]" + else: + param_data["type"] = param_type + else: + param_data["type"] = param_type # Extract description and sanitize markdown within it if "metadata" in param_info and "description" in param_info["metadata"]: diff --git a/src/000-cloud/020-observability/bicep/README.md b/src/000-cloud/020-observability/bicep/README.md index d8a06227..a21a1ddd 100644 --- a/src/000-cloud/020-observability/bicep/README.md +++ b/src/000-cloud/020-observability/bicep/README.md @@ -73,6 +73,7 @@ Common settings for the components. | logAnalyticsName | `string` | The Log Analytics Workspace name. | | logAnalyticsId | `string` | The Log Analytics Workspace ID. | | grafanaName | `string` | The Azure Managed Grafana name. | +| grafanaEndpoint | `string` | The Azure Managed Grafana endpoint. | | metricsDataCollectionRuleName | `string` | The metrics data collection rule name. | | logsDataCollectionRuleName | `string` | The logs data collection rule name. | | monitorPrivateLinkScopeId | `string` | Azure Monitor Private Link Scope resource ID. | diff --git a/src/000-cloud/020-observability/bicep/main.bicep b/src/000-cloud/020-observability/bicep/main.bicep index bb2d8f55..b26f4bae 100644 --- a/src/000-cloud/020-observability/bicep/main.bicep +++ b/src/000-cloud/020-observability/bicep/main.bicep @@ -512,6 +512,9 @@ output logAnalyticsId string = logAnalytics.id @description('The Azure Managed Grafana name.') output grafanaName string = grafana.name +@description('The Azure Managed Grafana endpoint.') +output grafanaEndpoint string = grafana.properties.endpoint + @description('The metrics data collection rule name.') output metricsDataCollectionRuleName string = metricsDataCollectionRule.name diff --git a/src/000-cloud/040-messaging/terraform/modules/eventhub/main.tf b/src/000-cloud/040-messaging/terraform/modules/eventhub/main.tf index 73a8a1e1..3936f9e4 100644 --- a/src/000-cloud/040-messaging/terraform/modules/eventhub/main.tf +++ b/src/000-cloud/040-messaging/terraform/modules/eventhub/main.tf @@ -11,6 +11,8 @@ resource "azurerm_eventhub_namespace" "destination_eventhub_namespace" { sku = "Standard" capacity = var.capacity + + local_authentication_enabled = false } resource "azurerm_eventhub" "destination_eh" { diff --git a/src/000-cloud/055-vpn-gateway/bicep/README.md b/src/000-cloud/055-vpn-gateway/bicep/README.md index 2c0f8cdb..cafec8d7 100644 --- a/src/000-cloud/055-vpn-gateway/bicep/README.md +++ b/src/000-cloud/055-vpn-gateway/bicep/README.md @@ -8,19 +8,19 @@ Ths component currently only supports Azure AD (Entra ID) authentication for Poi ## Parameters -| Name | Description | Type | Default | Required | -|:-----------------------------|:---------------------------------------------------------------|:---------------------------------------------|:-------------------------------------------|:---------| -| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | -| vpnGatewayConfig | VPN Gateway configuration settings. | `[_1.VpnGatewayConfig](#user-defined-types)` | [variables('_1.vpnGatewayConfigDefaults')] | no | -| gatewaySubnetAddressPrefix | Gateway subnet address prefix. | `string` | 10.0.2.0/27 | no | -| virtualNetworkName | Virtual network name for Gateway subnet creation. | `string` | n/a | yes | -| azureAdConfig | Azure AD configuration for VPN Gateway authentication. | `[_1.AzureAdConfig](#user-defined-types)` | [variables('_1.azureAdConfigDefaults')] | no | -| defaultOutboundAccessEnabled | Whether default outbound access is enabled for Gateway subnet. | `bool` | `false` | no | -| tags | Resource tags. | `object` | {} | no | -| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no | -| vpnSiteConnections | Site-to-site VPN connection definitions. | `array` | [] | no | -| vpnSiteDefaultIpsecPolicy | Fallback IPsec policy applied when sites omit an override. | `[_1.VpnIpsecPolicy](#user-defined-types)` | n/a | no | -| vpnSiteSharedKeys | Pre-shared keys keyed by sharedKeyReference values. | `secureObject` | {} | no | +| Name | Description | Type | Default | Required | +|:-----------------------------|:---------------------------------------------------------------|:------------------------------------------------|:-------------------------------------------|:---------| +| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | +| vpnGatewayConfig | VPN Gateway configuration settings. | `[_1.VpnGatewayConfig](#user-defined-types)` | [variables('_1.vpnGatewayConfigDefaults')] | no | +| gatewaySubnetAddressPrefix | Gateway subnet address prefix. | `string` | 10.0.2.0/27 | no | +| virtualNetworkName | Virtual network name for Gateway subnet creation. | `string` | n/a | yes | +| azureAdConfig | Azure AD configuration for VPN Gateway authentication. | `[_1.AzureAdConfig](#user-defined-types)` | [variables('_1.azureAdConfigDefaults')] | no | +| defaultOutboundAccessEnabled | Whether default outbound access is enabled for Gateway subnet. | `bool` | `false` | no | +| tags | Resource tags. | `object` | {} | no | +| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no | +| vpnSiteConnections | Site-to-site VPN connection definitions. | `[_1.VpnSiteConnection](#user-defined-types)[]` | [] | no | +| vpnSiteDefaultIpsecPolicy | Fallback IPsec policy applied when sites omit an override. | `[_1.VpnIpsecPolicy](#user-defined-types)` | n/a | no | +| vpnSiteSharedKeys | Pre-shared keys keyed by sharedKeyReference values. | `secureObject` | {} | no | ## Resources diff --git a/src/000-cloud/070-kubernetes/bicep/README.md b/src/000-cloud/070-kubernetes/bicep/README.md index b229a93b..16659d32 100644 --- a/src/000-cloud/070-kubernetes/bicep/README.md +++ b/src/000-cloud/070-kubernetes/bicep/README.md @@ -44,17 +44,16 @@ Creates subnets for AKS private endpoints in an existing Virtual Network. #### Parameters for network -| Name | Description | Type | Default | Required | -|:-----------------------------|:---------------------------------------------------------------------------------------------|:-----------------------------------|:--------|:---------| -| common | The common component configuration. | `[_1.Common](#user-defined-types)` | n/a | yes | -| virtualNetworkName | Virtual network name for subnet creation. | `string` | n/a | yes | -| networkSecurityGroupName | Network security group name to apply to the subnets. | `string` | n/a | yes | -| subnetAddressPrefixAks | Address prefix for the AKS system node subnet. | `string` | n/a | yes | -| subnetAddressPrefixAksPod | Address prefix for the AKS pod subnet. | `string` | n/a | yes | -| defaultOutboundAccessEnabled | Whether to enable default outbound internet access for AKS subnets. | `bool` | n/a | yes | -| shouldEnableNatGateway | Whether to associate AKS subnets with a NAT gateway for managed outbound egress. | `bool` | n/a | yes | -| natGatewayId | NAT gateway ID for associating AKS subnets. | `string` | n/a | no | -| shouldEnablePrivateEndpoint | Whether to enable private endpoint for AKS cluster; when true, subnet delegation is created. | `bool` | n/a | yes | +| Name | Description | Type | Default | Required | +|:-----------------------------|:---------------------------------------------------------------------------------|:-----------------------------------|:--------|:---------| +| common | The common component configuration. | `[_1.Common](#user-defined-types)` | n/a | yes | +| virtualNetworkName | Virtual network name for subnet creation. | `string` | n/a | yes | +| networkSecurityGroupName | Network security group name to apply to the subnets. | `string` | n/a | yes | +| subnetAddressPrefixAks | Address prefix for the AKS system node subnet. | `string` | n/a | yes | +| subnetAddressPrefixAksPod | Address prefix for the AKS pod subnet. | `string` | n/a | yes | +| defaultOutboundAccessEnabled | Whether to enable default outbound internet access for AKS subnets. | `bool` | n/a | yes | +| shouldEnableNatGateway | Whether to associate AKS subnets with a NAT gateway for managed outbound egress. | `bool` | n/a | yes | +| natGatewayId | NAT gateway ID for associating AKS subnets. | `string` | n/a | no | #### Resources for network diff --git a/src/000-cloud/070-kubernetes/bicep/main.bicep b/src/000-cloud/070-kubernetes/bicep/main.bicep index 0e9f19c4..84c3aef3 100644 --- a/src/000-cloud/070-kubernetes/bicep/main.bicep +++ b/src/000-cloud/070-kubernetes/bicep/main.bicep @@ -87,7 +87,6 @@ module network './modules/network.bicep' = { defaultOutboundAccessEnabled: aksNetworkConfig.defaultOutboundAccessEnabled shouldEnableNatGateway: aksNetworkConfig.shouldEnableNatGateway natGatewayId: natGatewayId - shouldEnablePrivateEndpoint: aksPrivateClusterConfig.shouldEnablePrivateEndpoint } } diff --git a/src/000-cloud/070-kubernetes/bicep/modules/network.bicep b/src/000-cloud/070-kubernetes/bicep/modules/network.bicep index 658aed75..62449f77 100644 --- a/src/000-cloud/070-kubernetes/bicep/modules/network.bicep +++ b/src/000-cloud/070-kubernetes/bicep/modules/network.bicep @@ -35,9 +35,6 @@ param shouldEnableNatGateway bool @description('NAT gateway ID for associating AKS subnets.') param natGatewayId string? -@description('Whether to enable private endpoint for AKS cluster; when true, subnet delegation is created.') -param shouldEnablePrivateEndpoint bool - /* Local Variables */ @@ -88,16 +85,15 @@ resource snetAksPod 'Microsoft.Network/virtualNetworks/subnets@2025-01-01' = { id: natGatewayId! } : null - delegations: shouldEnablePrivateEndpoint - ? [ - { - name: 'aks-delegation' - properties: { - serviceName: 'Microsoft.ContainerService/managedClusters' - } - } - ] - : [] + // Delegation required for Azure CNI with dedicated pod subnet (podSubnetID) + delegations: [ + { + name: 'aks-delegation' + properties: { + serviceName: 'Microsoft.ContainerService/managedClusters' + } + } + ] } dependsOn: [ snetAks // Make sure subnets are created in sequence to avoid conflicts diff --git a/src/000-cloud/070-kubernetes/terraform/main.tf b/src/000-cloud/070-kubernetes/terraform/main.tf index dbf261f9..2a29fa44 100644 --- a/src/000-cloud/070-kubernetes/terraform/main.tf +++ b/src/000-cloud/070-kubernetes/terraform/main.tf @@ -45,7 +45,6 @@ module "network" { subnet_address_prefixes_aks = var.subnet_address_prefixes_aks subnet_address_prefixes_aks_pod = var.subnet_address_prefixes_aks_pod node_pools = var.node_pools - should_enable_private_endpoint = var.should_enable_private_endpoint } diff --git a/src/000-cloud/070-kubernetes/terraform/modules/network/README.md b/src/000-cloud/070-kubernetes/terraform/modules/network/README.md index 6d5dab6d..bd160e27 100644 --- a/src/000-cloud/070-kubernetes/terraform/modules/network/README.md +++ b/src/000-cloud/070-kubernetes/terraform/modules/network/README.md @@ -34,21 +34,20 @@ Deploys virtual network resources for AKS ## Inputs -| Name | Description | Type | Default | Required | -|-------------------------------------|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|---------|:--------:| -| default\_outbound\_access\_enabled | Whether to enable default outbound internet access for AKS subnets | `bool` | n/a | yes | -| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes | -| instance | Instance identifier for naming resources: 001, 002, etc | `string` | n/a | yes | -| nat\_gateway\_id | NAT gateway resource id for associating AKS subnets | `string` | n/a | yes | -| network\_security\_group | n/a | ```object({ id = string })``` | n/a | yes | -| resource\_group | Resource group object containing name and id where resources will be deployed | ```object({ name = string })``` | n/a | yes | -| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes | -| should\_enable\_nat\_gateway | Whether to associate AKS subnets with a NAT gateway for managed egress | `bool` | n/a | yes | -| should\_enable\_private\_endpoint | Whether to enable private endpoint for AKS cluster; when true, subnet delegations are created | `bool` | n/a | yes | -| subnet\_address\_prefixes\_aks | Address prefixes for the AKS subnet | `list(string)` | n/a | yes | -| subnet\_address\_prefixes\_aks\_pod | Address prefixes for the AKS pod subnet | `list(string)` | n/a | yes | -| virtual\_network | n/a | ```object({ name = string })``` | n/a | yes | -| node\_pools | Configuration for additional node pool subnets; map key is used as the node pool name | ```map(object({ subnet_address_prefixes = list(string) pod_subnet_address_prefixes = list(string) }))``` | `{}` | no | +| Name | Description | Type | Default | Required | +|-------------------------------------|---------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|---------|:--------:| +| default\_outbound\_access\_enabled | Whether to enable default outbound internet access for AKS subnets | `bool` | n/a | yes | +| environment | Environment for all resources in this module: dev, test, or prod | `string` | n/a | yes | +| instance | Instance identifier for naming resources: 001, 002, etc | `string` | n/a | yes | +| nat\_gateway\_id | NAT gateway resource id for associating AKS subnets | `string` | n/a | yes | +| network\_security\_group | n/a | ```object({ id = string })``` | n/a | yes | +| resource\_group | Resource group object containing name and id where resources will be deployed | ```object({ name = string })``` | n/a | yes | +| resource\_prefix | Prefix for all resources in this module | `string` | n/a | yes | +| should\_enable\_nat\_gateway | Whether to associate AKS subnets with a NAT gateway for managed egress | `bool` | n/a | yes | +| subnet\_address\_prefixes\_aks | Address prefixes for the AKS subnet | `list(string)` | n/a | yes | +| subnet\_address\_prefixes\_aks\_pod | Address prefixes for the AKS pod subnet | `list(string)` | n/a | yes | +| virtual\_network | n/a | ```object({ name = string })``` | n/a | yes | +| node\_pools | Configuration for additional node pool subnets; map key is used as the node pool name | ```map(object({ subnet_address_prefixes = list(string) pod_subnet_address_prefixes = list(string) }))``` | `{}` | no | ## Outputs diff --git a/src/000-cloud/070-kubernetes/terraform/modules/network/main.tf b/src/000-cloud/070-kubernetes/terraform/modules/network/main.tf index 91e5ccf7..de3af6c6 100644 --- a/src/000-cloud/070-kubernetes/terraform/modules/network/main.tf +++ b/src/000-cloud/070-kubernetes/terraform/modules/network/main.tf @@ -41,14 +41,11 @@ resource "azurerm_subnet" "snet_aks_pod" { address_prefixes = var.subnet_address_prefixes_aks_pod default_outbound_access_enabled = var.default_outbound_access_enabled - dynamic "delegation" { - for_each = var.should_enable_private_endpoint ? [1] : [] - content { - name = "aks-delegation" - service_delegation { - name = "Microsoft.ContainerService/managedClusters" - actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"] - } + delegation { + name = "aks-delegation" + service_delegation { + name = "Microsoft.ContainerService/managedClusters" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"] } } } @@ -86,14 +83,11 @@ resource "azurerm_subnet" "snet_aks_node_pool_pod" { address_prefixes = each.value.pod_subnet_address_prefixes default_outbound_access_enabled = var.default_outbound_access_enabled - dynamic "delegation" { - for_each = var.should_enable_private_endpoint ? [1] : [] - content { - name = "aks-delegation" - service_delegation { - name = "Microsoft.ContainerService/managedClusters" - actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"] - } + delegation { + name = "aks-delegation" + service_delegation { + name = "Microsoft.ContainerService/managedClusters" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"] } } } diff --git a/src/000-cloud/070-kubernetes/terraform/modules/network/variables.tf b/src/000-cloud/070-kubernetes/terraform/modules/network/variables.tf index 6839150c..76f7e9c5 100644 --- a/src/000-cloud/070-kubernetes/terraform/modules/network/variables.tf +++ b/src/000-cloud/070-kubernetes/terraform/modules/network/variables.tf @@ -31,8 +31,3 @@ variable "node_pools" { description = "Configuration for additional node pool subnets; map key is used as the node pool name" default = {} } - -variable "should_enable_private_endpoint" { - type = bool - description = "Whether to enable private endpoint for AKS cluster; when true, subnet delegations are created" -} diff --git a/src/000-cloud/085-ai-foundry/bicep/README.md b/src/000-cloud/085-ai-foundry/bicep/README.md index 842a43fc..1b2a7c78 100644 --- a/src/000-cloud/085-ai-foundry/bicep/README.md +++ b/src/000-cloud/085-ai-foundry/bicep/README.md @@ -7,19 +7,19 @@ Deploys Microsoft Foundry account with optional projects, model deployments, RAI ## Parameters -| Name | Description | Type | Default | Required | -|:----------------------------|:-------------------------------------------------------------------------------------------------------------|:--------------------------------------------|:------------------------------------------|:---------| -| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | -| aiFoundryName | Name for the AI Foundry account. If not provided, defaults to aif-{resourcePrefix}-{environment}-{instance}. | `string` | n/a | no | -| aiFoundryConfig | Configuration settings for the Microsoft Foundry account. | `[_1.AiFoundryConfig](#user-defined-types)` | [variables('_1.aiFoundryConfigDefaults')] | no | -| tags | Tags to apply to all resources. | `object` | {} | no | -| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no | -| aiProjects | Array of AI Foundry projects to create. | `array` | [] | no | -| raiPolicies | Array of RAI policies to create. | `array` | [] | no | -| modelDeployments | Array of model deployments to create. | `array` | [] | no | -| shouldCreatePrivateEndpoint | Whether to create a private endpoint for the Microsoft Foundry account. | `bool` | `false` | no | -| privateEndpointSubnetId | Subnet ID for the private endpoint. | `string` | | no | -| virtualNetworkId | Virtual network ID for DNS zone links. | `string` | | no | +| Name | Description | Type | Default | Required | +|:----------------------------|:-------------------------------------------------------------------------------------------------------------|:----------------------------------------------|:------------------------------------------|:---------| +| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | +| aiFoundryName | Name for the AI Foundry account. If not provided, defaults to aif-{resourcePrefix}-{environment}-{instance}. | `string` | n/a | no | +| aiFoundryConfig | Configuration settings for the Microsoft Foundry account. | `[_1.AiFoundryConfig](#user-defined-types)` | [variables('_1.aiFoundryConfigDefaults')] | no | +| tags | Tags to apply to all resources. | `object` | {} | no | +| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no | +| aiProjects | Array of AI Foundry projects to create. | `[_1.AiProject](#user-defined-types)[]` | [] | no | +| raiPolicies | Array of RAI policies to create. | `[_1.RaiPolicy](#user-defined-types)[]` | [] | no | +| modelDeployments | Array of model deployments to create. | `[_1.ModelDeployment](#user-defined-types)[]` | [] | no | +| shouldCreatePrivateEndpoint | Whether to create a private endpoint for the Microsoft Foundry account. | `bool` | `false` | no | +| privateEndpointSubnetId | Subnet ID for the private endpoint. | `string` | | no | +| virtualNetworkId | Virtual network ID for DNS zone links. | `string` | | no | ## Resources diff --git a/src/100-edge/100-cncf-cluster/bicep/README.md b/src/100-edge/100-cncf-cluster/bicep/README.md index 0299aa80..8959505f 100644 --- a/src/100-edge/100-cncf-cluster/bicep/README.md +++ b/src/100-edge/100-cncf-cluster/bicep/README.md @@ -21,7 +21,7 @@ The scripts handle primary and secondary node(s) setup, cluster administration, | shouldEnableArcAutoUpgrade | Whether to enable auto-upgrade for Azure Arc agents. | `bool` | [not(equals(parameters('common').environment, 'prod'))] | no | | clusterAdminOid | The Object ID that will be given cluster-admin permissions. | `string` | n/a | no | | clusterAdminUpn | The User Principal Name that will be given cluster-admin permissions. | `string` | n/a | no | -| clusterNodeVirtualMachineNames | The node virtual machines names. | `array` | n/a | no | +| clusterNodeVirtualMachineNames | The node virtual machines names. | `string[]` | n/a | no | | clusterServerVirtualMachineName | The server virtual machines name. | `string` | n/a | no | | clusterServerHostMachineUsername | Username used for the host machines that will be given kube-config settings on setup. (Otherwise, resource_prefix if it exists as a user) | `string` | [parameters('common').resourcePrefix] | no | | clusterServerIp | The IP address for the server for the cluster. (Needed for mult-node cluster) | `string` | n/a | no | diff --git a/src/100-edge/110-iot-ops/bicep/README.md b/src/100-edge/110-iot-ops/bicep/README.md index 0862c5be..927a11fb 100644 --- a/src/100-edge/110-iot-ops/bicep/README.md +++ b/src/100-edge/110-iot-ops/bicep/README.md @@ -7,49 +7,50 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure ## Parameters -| Name | Description | Type | Default | Required | -|:------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------|:---------| -| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | -| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | n/a | yes | -| secretStoreConfig | The settings for the Secret Store Extension. | `[_1.SecretStoreExtension](#user-defined-types)` | [variables('_1.secretStoreExtensionDefaults')] | no | -| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | -| aioIdentityName | The name of the User Assigned Managed Identity for Azure IoT Operations. | `string` | n/a | yes | -| aioExtensionConfig | The settings for the Azure IoT Operations Extension. | `[_1.AioExtension](#user-defined-types)` | [variables('_1.aioExtensionDefaults')] | no | -| aioFeatures | AIO Instance features. | `[_1.AioFeatures](#user-defined-types)` | n/a | no | -| aioInstanceName | The name for the Azure IoT Operations Instance resource. | `string` | [format('{0}-ops-instance', parameters('arcConnectedClusterName'))] | no | -| aioDataFlowInstanceConfig | The settings for Azure IoT Operations Data Flow Instances. | `[_1.AioDataFlowInstance](#user-defined-types)` | [variables('_1.aioDataFlowInstanceDefaults')] | no | -| aioMqBrokerConfig | The settings for the Azure IoT Operations MQ Broker. | `[_1.AioMqBroker](#user-defined-types)` | [variables('_1.aioMqBrokerDefaults')] | no | -| brokerListenerAnonymousConfig | Configuration for the insecure anonymous AIO MQ Broker Listener. | `[_1.AioMqBrokerAnonymous](#user-defined-types)` | [variables('_1.aioMqBrokerAnonymousDefaults')] | no | -| configurationSettingsOverride | Optional configuration settings to override default IoT Operations extension configuration. Use the same key names as the az iot ops --ops-config parameter. | `object` | {} | no | -| schemaRegistryName | The resource name for the ADR Schema Registry for Azure IoT Operations. | `string` | n/a | yes | -| adrNamespaceName | The resource name for the ADR Namespace for Azure IoT Operations. | `string` | n/a | no | -| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | -| shouldDeployResourceSyncRules | Whether or not to deploy the Custom Locations Resource Sync Rules for the Azure IoT Operations resources. | `bool` | `true` | no | -| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | -| shouldEnableOtelCollector | Whether or not to enable the Open Telemetry Collector for Azure IoT Operations. | `bool` | `true` | no | -| shouldEnableOpcUaSimulator | Whether or not to enable the OPC UA Simulator for Azure IoT Operations. | `bool` | `true` | no | -| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | -| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | -| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `array` | [] | no | -| akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no | -| customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | -| additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `array` | [] | no | -| trustIssuerSettings | The trust issuer settings for Customer Managed Azure IoT Operations Settings. | `[_1.TrustIssuerConfig](#user-defined-types)` | {'trustSource': 'SelfSigned'} | no | -| sseKeyVaultName | The name of the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | n/a | yes | -| sseIdentityName | The name of the User Assigned Managed Identity for Secret Sync. | `string` | n/a | yes | -| sseKeyVaultResourceGroupName | The name of the Resource Group for the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | [resourceGroup().name] | no | -| shouldAssignSseKeyVaultRoles | Whether to assign roles for Key Vault to the provided Secret Sync Identity. | `bool` | `true` | no | -| shouldAssignDeployIdentityRoles | Whether to assign roles to the deploy identity. | `bool` | [not(empty(parameters('deployIdentityName')))] | no | -| deployIdentityName | The resource name for a managed identity that will be given deployment admin permissions. | `string` | n/a | no | -| shouldDeployAioDeploymentScripts | Whether to deploy DeploymentScripts for Azure IoT Operations. | `bool` | `false` | no | -| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | [parameters('sseKeyVaultName')] | no | -| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [parameters('sseKeyVaultResourceGroupName')] | no | -| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | deploy-user-token | no | -| deploymentScriptsSecretNamePrefix | The prefix used with constructing the secret name that will have the deployment script. | `string` | [format('{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | -| shouldAddDeployScriptsToKeyVault | Whether to add the deploy scripts for DeploymentScripts to Key Vault as secrets. (Required for DeploymentScripts) | `bool` | `false` | no | -| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no | +| Name | Description | Type | Default | Required | +|:------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------|:---------| +| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | +| arcConnectedClusterName | The resource name for the Arc connected cluster. | `string` | n/a | yes | +| secretStoreConfig | The settings for the Secret Store Extension. | `[_1.SecretStoreExtension](#user-defined-types)` | [variables('_1.secretStoreExtensionDefaults')] | no | +| shouldInitAio | Whether to deploy the Azure IoT Operations initial connected cluster resources, Secret Sync, ACSA, OSM, AIO Platform. | `bool` | `true` | no | +| aioIdentityName | The name of the User Assigned Managed Identity for Azure IoT Operations. | `string` | n/a | yes | +| aioExtensionConfig | The settings for the Azure IoT Operations Extension. | `[_1.AioExtension](#user-defined-types)` | [variables('_1.aioExtensionDefaults')] | no | +| aioFeatures | AIO Instance features. | `[_1.AioFeatures](#user-defined-types)` | n/a | no | +| aioInstanceName | The name for the Azure IoT Operations Instance resource. | `string` | [format('{0}-ops-instance', parameters('arcConnectedClusterName'))] | no | +| aioDataFlowInstanceConfig | The settings for Azure IoT Operations Data Flow Instances. | `[_1.AioDataFlowInstance](#user-defined-types)` | [variables('_1.aioDataFlowInstanceDefaults')] | no | +| aioMqBrokerConfig | The settings for the Azure IoT Operations MQ Broker. | `[_1.AioMqBroker](#user-defined-types)` | [variables('_1.aioMqBrokerDefaults')] | no | +| brokerListenerAnonymousConfig | Configuration for the insecure anonymous AIO MQ Broker Listener. | `[_1.AioMqBrokerAnonymous](#user-defined-types)` | [variables('_1.aioMqBrokerAnonymousDefaults')] | no | +| configurationSettingsOverride | Optional configuration settings to override default IoT Operations extension configuration. Use the same key names as the az iot ops --ops-config parameter. | `object` | {} | no | +| schemaRegistryName | The resource name for the ADR Schema Registry for Azure IoT Operations. | `string` | n/a | yes | +| adrNamespaceName | The resource name for the ADR Namespace for Azure IoT Operations. | `string` | n/a | no | +| shouldDeployAio | Whether to deploy an Azure IoT Operations Instance and all of its required components into the connected cluster. | `bool` | `true` | no | +| shouldDeployResourceSyncRules | Whether or not to deploy the Custom Locations Resource Sync Rules for the Azure IoT Operations resources. | `bool` | `true` | no | +| shouldCreateAnonymousBrokerListener | Whether to enable an insecure anonymous AIO MQ Broker Listener. (Should only be used for dev or test environments) | `bool` | `false` | no | +| shouldEnableOtelCollector | Whether or not to enable the Open Telemetry Collector for Azure IoT Operations. | `bool` | `true` | no | +| shouldEnableOpcUaSimulator | Whether or not to enable the OPC UA Simulator for Azure IoT Operations. | `bool` | `true` | no | +| shouldEnableAkriRestConnector | Deploy Akri REST HTTP Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriMediaConnector | Deploy Akri Media Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriOnvifConnector | Deploy Akri ONVIF Connector template to the IoT Operations instance. | `bool` | `false` | no | +| shouldEnableAkriSseConnector | Deploy Akri SSE Connector template to the IoT Operations instance. | `bool` | `false` | no | +| customAkriConnectors | List of custom Akri connector templates with user-defined endpoint types and container images. | `[_1.AkriConnectorTemplate](#user-defined-types)[]` | [] | no | +| registryEndpoints | List of additional container registry endpoints for pulling custom artifacts. MCR is always added automatically. | `[_1.RegistryEndpointConfig](#user-defined-types)[]` | [] | no | +| akriMqttSharedConfig | Shared MQTT connection configuration for all Akri connectors. | `[_1.AkriMqttConfig](#user-defined-types)` | {'host': 'aio-broker:18883', 'audience': 'aio-internal', 'caConfigmap': 'azure-iot-operations-aio-ca-trust-bundle'} | no | +| customLocationName | The name for the Custom Locations resource. | `string` | [format('{0}-cl', parameters('arcConnectedClusterName'))] | no | +| additionalClusterExtensionIds | Additional cluster extension IDs to include in the custom location. (Appended to the default Secret Store and IoT Operations extension IDs) | `string[]` | [] | no | +| trustIssuerSettings | The trust issuer settings for Customer Managed Azure IoT Operations Settings. | `[_1.TrustIssuerConfig](#user-defined-types)` | {'trustSource': 'SelfSigned'} | no | +| sseKeyVaultName | The name of the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | n/a | yes | +| sseIdentityName | The name of the User Assigned Managed Identity for Secret Sync. | `string` | n/a | yes | +| sseKeyVaultResourceGroupName | The name of the Resource Group for the Key Vault for Secret Sync. (Required when providing sseIdentityName) | `string` | [resourceGroup().name] | no | +| shouldAssignSseKeyVaultRoles | Whether to assign roles for Key Vault to the provided Secret Sync Identity. | `bool` | `true` | no | +| shouldAssignDeployIdentityRoles | Whether to assign roles to the deploy identity. | `bool` | [not(empty(parameters('deployIdentityName')))] | no | +| deployIdentityName | The resource name for a managed identity that will be given deployment admin permissions. | `string` | n/a | no | +| shouldDeployAioDeploymentScripts | Whether to deploy DeploymentScripts for Azure IoT Operations. | `bool` | `false` | no | +| deployKeyVaultName | The name of the Key Vault that will have scripts and secrets for deployment. | `string` | [parameters('sseKeyVaultName')] | no | +| deployKeyVaultResourceGroupName | The resource group name where the Key Vault is located. Defaults to the current resource group. | `string` | [parameters('sseKeyVaultResourceGroupName')] | no | +| deployUserTokenSecretName | The name for the deploy user token secret in Key Vault. | `string` | deploy-user-token | no | +| deploymentScriptsSecretNamePrefix | The prefix used with constructing the secret name that will have the deployment script. | `string` | [format('{0}-{1}-{2}', parameters('common').resourcePrefix, parameters('common').environment, parameters('common').instance)] | no | +| shouldAddDeployScriptsToKeyVault | Whether to add the deploy scripts for DeploymentScripts to Key Vault as secrets. (Required for DeploymentScripts) | `bool` | `false` | no | +| telemetry_opt_out | Whether to opt out of telemetry data collection. | `bool` | `false` | no | ## Resources @@ -63,6 +64,7 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | postInitScripts | `Microsoft.Resources/deployments` | 2025-04-01 | | iotOpsInstance | `Microsoft.Resources/deployments` | 2025-04-01 | | akriConnectors | `Microsoft.Resources/deployments` | 2025-04-01 | +| registryEndpointsModule | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScriptsSecrets | `Microsoft.Resources/deployments` | 2025-04-01 | | postInstanceScripts | `Microsoft.Resources/deployments` | 2025-04-01 | @@ -78,6 +80,7 @@ Deploys Azure IoT Operations extensions, instances, and configurations on Azure | postInitScripts | Runs deployment scripts for IoT Operations using an Azure deploymentScript resource, including tool installation and script execution. | | iotOpsInstance | Deploys Azure IoT Operations instance, broker, authentication, listeners, and data flow components on an Azure Arc-enabled Kubernetes cluster. | | akriConnectors | Deploys multiple Azure IoT Operations Akri Connector Templates as part of the IoT Operations deployment. Supports REST/HTTP, Media, ONVIF, and SSE connector types with configurable runtime and MQTT settings. | +| registryEndpointsModule | Manages container registry endpoints for Azure IoT Operations, including the default MCR endpoint, custom registry endpoints, and ACR role assignments. | | postInstanceScriptsSecrets | Creates secrets in Key Vault for deployment script setup and initialization for Azure IoT Operations. | | postInstanceScripts | Runs deployment scripts for IoT Operations using an Azure deploymentScript resource, including tool installation and script execution. | @@ -294,7 +297,6 @@ Deploys Azure IoT Operations instance, broker, authentication, listeners, and da | adrSyncRule | `Microsoft.ExtendedLocation/customLocations/resourceSyncRules` | 2021-08-31-preview | | defaultSecretSyncSecretProviderClass | `Microsoft.SecretSyncController/azureKeyVaultSecretProviderClasses` | 2024-08-21-preview | | aioInstance | `Microsoft.IoTOperations/instances` | 2025-10-01 | -| registryEndpoint | `Microsoft.IoTOperations/instances/registryEndpoints` | 2025-10-01 | | broker | `Microsoft.IoTOperations/instances/brokers` | 2025-10-01 | | brokerAuthn | `Microsoft.IoTOperations/instances/brokers/authentications` | 2025-10-01 | | brokerListener | `Microsoft.IoTOperations/instances/brokers/listeners` | 2025-10-01 | @@ -328,6 +330,7 @@ Deploys Azure IoT Operations instance, broker, authentication, listeners, and da | brokerAuthnId | `string` | The ID of the deployed AIO MQ Broker Authentication. | | brokerListenerName | `string` | The name of the deployed AIO MQ Broker Listener. | | brokerListenerId | `string` | The ID of the deployed AIO MQ Broker Listener. | +| extensionIdentity | `object` | AIO Arc extension identity information for role assignments. | ### akriConnectors @@ -355,6 +358,35 @@ Deploys multiple Azure IoT Operations Akri Connector Templates as part of the Io | connectorTemplates | `array` | Map of deployed connector templates by name with id and type. | | connectorTypesDeployed | `array` | List of connector types that were deployed. | +### registryEndpointsModule + +Manages container registry endpoints for Azure IoT Operations, including the default MCR endpoint, custom registry endpoints, and ACR role assignments. + +#### Parameters for registryEndpointsModule + +| Name | Description | Type | Default | Required | +|:---------------------|:------------------------------------------------------------------|:---------|:--------|:---------| +| aioInstanceId | Azure IoT Operations instance ID (parent for registry endpoints). | `string` | n/a | yes | +| customLocationId | Custom location ID for the Azure IoT Operations deployment. | `string` | n/a | yes | +| extensionPrincipalId | Principal ID of the AIO Arc extension for ACR role assignments. | `string` | n/a | yes | +| registryEndpoints | List of custom registry endpoints to configure. | `array` | n/a | yes | + +#### Resources for registryEndpointsModule + +| Name | Type | API Version | +|:-----------------------|:------------------------------------------------------|:------------| +| mcrEndpoint | `Microsoft.IoTOperations/instances/registryEndpoints` | 2025-10-01 | +| customEndpoints | `Microsoft.IoTOperations/instances/registryEndpoints` | 2025-10-01 | +| acrPullRoleAssignments | `Microsoft.Authorization/roleAssignments` | 2022-04-01 | + +#### Outputs for registryEndpointsModule + +| Name | Type | Description | +|:----------------------|:---------|:------------------------------------| +| mcrEndpoint | `object` | Default MCR registry endpoint. | +| customEndpointsOutput | `array` | Array of custom registry endpoints. | +| acrRoleAssignments | `array` | Array of ACR role assignment IDs. | + ### postInstanceScriptsSecrets Creates secrets in Key Vault for deployment script setup and initialization for Azure IoT Operations. @@ -549,6 +581,14 @@ Trust settings for Akri connector. |:-------------------|:---------|:------------------------------------| | trustListSecretRef | `string` | Reference to the trust list secret. | +### `_1.ArtifactPullSecretSettings` + +Authentication settings for Artifact Pull Secret. + +| Property | Type | Description | +|:----------|:---------|:--------------------------------------------------------------------------| +| secretRef | `string` | The name of the kubernetes secret that contains the artifact pull secret. | + ### `_1.BrokerPersistence` Broker persistence configuration for disk-backed message storage. @@ -608,6 +648,57 @@ The mode of the AIO instance feature. Either "Stable", "Preview" or "Disabled". The setting value of the AIO instance feature. Either "Enabled" or "Disabled". +### `_1.RegistryAuthAnonymous` + +Anonymous authentication for registry endpoint. + +| Property | Type | Description | +|:------------------|:---------|:--------------------------------------------------| +| method | `string` | Authentication method. | +| anonymousSettings | `object` | Anonymous authentication settings (empty object). | + +### `_1.RegistryAuthArtifactPullSecret` + +Artifact Pull Secret authentication for registry endpoint. + +| Property | Type | Description | +|:---------------------------|:-------------------------------------------------------|:-------------------------------| +| method | `string` | Authentication method. | +| artifactPullSecretSettings | `[_1.ArtifactPullSecretSettings](#user-defined-types)` | Artifact pull secret settings. | + +### `_1.RegistryAuthentication` + +Authentication configuration for a registry endpoint. + +### `_1.RegistryAuthSystemAssignedManagedIdentity` + +System-Assigned Managed Identity authentication for registry endpoint. + +| Property | Type | Description | +|:--------------------------------------|:------------------------------------------------------------------|:-------------------------------------------| +| method | `string` | Authentication method. | +| systemAssignedManagedIdentitySettings | `[_1.SystemAssignedManagedIdentitySettings](#user-defined-types)` | System-assigned managed identity settings. | + +### `_1.RegistryAuthUserAssignedManagedIdentity` + +User-Assigned Managed Identity authentication for registry endpoint. + +| Property | Type | Description | +|:------------------------------------|:----------------------------------------------------------------|:-----------------------------------------| +| method | `string` | Authentication method. | +| userAssignedManagedIdentitySettings | `[_1.UserAssignedManagedIdentitySettings](#user-defined-types)` | User-assigned managed identity settings. | + +### `_1.RegistryEndpointConfig` + +Container registry endpoint configuration for AIO instance. + +| Property | Type | Description | +|:---------------|:---------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------| +| name | `string` | Unique name for the registry endpoint (3-63 chars, lowercase alphanumeric and hyphens). | +| host | `string` | Container registry hostname (e.g., myregistry.azurecr.io). | +| acrResourceId | `string` | Optional ACR resource ID for automatic AcrPull role assignment. Only applicable when authentication.method is SystemAssignedManagedIdentity. | +| authentication | `[_1.RegistryAuthentication](#user-defined-types)` | Authentication configuration for the registry. | + ### `_1.Release` The common settings for Azure Arc Extensions. @@ -661,6 +752,14 @@ The configuration for Self-Signed Issuer for Azure IoT Operations certificates. |:------------|:---------|:------------| | trustSource | `string` | | +### `_1.SystemAssignedManagedIdentitySettings` + +Authentication settings for System-Assigned Managed Identity. + +| Property | Type | Description | +|:---------|:---------|:--------------------------------------------------------------------------------------------------------| +| audience | `string` | Audience of the service to authenticate against. Defaults to "" for ACR. | + ### `_1.TrustConfigSource` The config source of trust for how to use or generate Azure IoT Operations certificates. @@ -684,6 +783,16 @@ The configuration for the trust settings of Azure IoT Operations certificates. The source of trust for Azure IoT Operations certificates. +### `_1.UserAssignedManagedIdentitySettings` + +Authentication settings for User-Assigned Managed Identity. + +| Property | Type | Description | +|:---------|:---------|:---------------------------------------------------------------| +| clientId | `string` | Client ID for the user-assigned managed identity. | +| tenantId | `string` | Tenant ID where the managed identity is located. | +| scope | `string` | Resource identifier (application ID URI) with .default suffix. | + ### `_2.Common` Common settings for the components. @@ -697,21 +806,22 @@ Common settings for the components. ## Outputs -| Name | Type | Description | -|:---------------------------|:---------|:-------------------------------------------------------------------| -| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | -| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | -| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | -| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | -| customLocationId | `string` | The ID of the deployed Custom Location. | -| customLocationName | `string` | The name of the deployed Custom Location. | -| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | -| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | -| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | -| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | -| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | -| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | -| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | +| Name | Type | Description | +|:---------------------------|:---------|:----------------------------------------------------------------------| +| aioPlatformExtensionId | `string` | The ID of the Azure IoT Operations Platform Extension. | +| aioPlatformExtensionName | `string` | The name of the Azure IoT Operations Platform Extension. | +| aioNamespace | `string` | The namespace in the cluster where Azure IoT Operations is installed. | +| secretStoreExtensionId | `string` | The ID of the Secret Store Extension. | +| secretStoreExtensionName | `string` | The name of the Secret Store Extension. | +| customLocationId | `string` | The ID of the deployed Custom Location. | +| customLocationName | `string` | The name of the deployed Custom Location. | +| aioInstanceId | `string` | The ID of the deployed Azure IoT Operations instance. | +| aioInstanceName | `string` | The name of the deployed Azure IoT Operations instance. | +| dataFlowProfileId | `string` | The ID of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowProfileName | `string` | The name of the deployed Azure IoT Operations Data Flow Profile. | +| dataFlowEndpointId | `string` | The ID of the deployed Azure IoT Operations Data Flow Endpoint. | +| dataFlowEndpointName | `string` | The name of the deployed Azure IoT Operations Data Flow Endpoint. | +| akriConnectorTemplates | `array` | Map of deployed Akri connector templates by name with id and type. | +| akriConnectorTypesDeployed | `array` | List of Akri connector types that were deployed. | \ No newline at end of file diff --git a/src/100-edge/110-iot-ops/bicep/main.bicep b/src/100-edge/110-iot-ops/bicep/main.bicep index 07089eb0..f5fe157a 100644 --- a/src/100-edge/110-iot-ops/bicep/main.bicep +++ b/src/100-edge/110-iot-ops/bicep/main.bicep @@ -93,6 +93,9 @@ param shouldEnableAkriSseConnector bool = false @description('List of custom Akri connector templates with user-defined endpoint types and container images.') param customAkriConnectors types.AkriConnectorTemplate[] = [] +@description('List of additional container registry endpoints for pulling custom artifacts. MCR is always added automatically.') +param registryEndpoints types.RegistryEndpointConfig[] = [] + @description('Shared MQTT connection configuration for all Akri connectors.') param akriMqttSharedConfig types.AkriMqttConfig = { host: 'aio-broker:18883' @@ -379,6 +382,20 @@ module akriConnectors 'modules/akri-connectors.bicep' = if (shouldDeployAkriConn } } +/* + Registry Endpoints Module +*/ + +module registryEndpointsModule 'modules/registry-endpoints.bicep' = if (shouldDeployAio) { + name: '${deployment().name}-regep5' + params: { + aioInstanceId: iotOpsInstance.?outputs.?aioInstanceId ?? '' + customLocationId: iotOpsInstance.?outputs.?customLocationId ?? '' + extensionPrincipalId: iotOpsInstance.?outputs.?extensionIdentity.?principalId ?? '' + registryEndpoints: registryEndpoints + } +} + /* Post Instance Script Modules */ @@ -429,6 +446,9 @@ output aioPlatformExtensionId string = shouldDeployAio ? (iotOpsInstance.?output @description('The name of the Azure IoT Operations Platform Extension.') output aioPlatformExtensionName string = shouldDeployAio ? (iotOpsInstance.?outputs.?aioExtensionName ?? '') : '' +@description('The namespace in the cluster where Azure IoT Operations is installed.') +output aioNamespace string = aioExtensionConfig.settings.namespace + @description('The ID of the Secret Store Extension.') output secretStoreExtensionId string = (iotOpsInit.?outputs.?secretStoreExtensionId) ?? '' diff --git a/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep b/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep index 91a40978..f9d62c75 100644 --- a/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep +++ b/src/100-edge/110-iot-ops/bicep/modules/iot-ops-instance.bicep @@ -320,22 +320,6 @@ resource aioInstance 'Microsoft.IoTOperations/instances@2025-10-01' = { ) } -resource registryEndpoint 'Microsoft.IoTOperations/instances/registryEndpoints@2025-10-01' = { - parent: aioInstance - name: 'default' - extendedLocation: { - name: customLocation.id - type: 'CustomLocation' - } - properties: { - host: 'mcr.microsoft.com' - authentication: { - method: 'Anonymous' - anonymousSettings: {} - } - } -} - resource broker 'Microsoft.IoTOperations/instances/brokers@2025-10-01' = { parent: aioInstance name: 'default' @@ -550,3 +534,8 @@ output brokerListenerName string = brokerListener.name @description('The ID of the deployed AIO MQ Broker Listener.') output brokerListenerId string = brokerListener.id + +@description('AIO Arc extension identity information for role assignments.') +output extensionIdentity object = { + principalId: aioExtension.identity.principalId +} diff --git a/src/100-edge/110-iot-ops/bicep/modules/registry-endpoints.bicep b/src/100-edge/110-iot-ops/bicep/modules/registry-endpoints.bicep new file mode 100644 index 00000000..c0355387 --- /dev/null +++ b/src/100-edge/110-iot-ops/bicep/modules/registry-endpoints.bicep @@ -0,0 +1,132 @@ +metadata name = 'Registry Endpoints Module' +metadata description = 'Manages container registry endpoints for Azure IoT Operations, including the default MCR endpoint, custom registry endpoints, and ACR role assignments.' + +import * as types from '../types.bicep' + +/* + Parameters +*/ + +@description('Azure IoT Operations instance ID (parent for registry endpoints).') +param aioInstanceId string + +@description('Custom location ID for the Azure IoT Operations deployment.') +param customLocationId string + +@description('Principal ID of the AIO Arc extension for ACR role assignments.') +param extensionPrincipalId string + +@description('List of custom registry endpoints to configure.') +param registryEndpoints types.RegistryEndpointConfig[] + +/* + Variables +*/ + +var aioInstanceName = last(split(aioInstanceId, '/'))! + +// Filter endpoints that have ACR resource ID and use SystemAssignedManagedIdentity +var endpointsWithAcr = filter( + registryEndpoints, + endpoint => endpoint.?acrResourceId != null && endpoint.authentication.method == 'SystemAssignedManagedIdentity' +) + +// Default audience for system-assigned managed identity using environment function +var defaultSystemAssignedManagedIdentityAudience = environment().resourceManager + +/* + Resources +*/ + +// Default MCR endpoint (always created) +resource mcrEndpoint 'Microsoft.IoTOperations/instances/registryEndpoints@2025-10-01' = { + name: '${aioInstanceName}/mcr' + extendedLocation: { + type: 'CustomLocation' + name: customLocationId + } + properties: { + host: 'mcr.microsoft.com' + authentication: { + method: 'Anonymous' + anonymousSettings: {} + } + } +} + +// Custom registry endpoints +resource customEndpoints 'Microsoft.IoTOperations/instances/registryEndpoints@2025-10-01' = [ + for endpoint in registryEndpoints: { + name: '${aioInstanceName}/${endpoint.name}' + extendedLocation: { + type: 'CustomLocation' + name: customLocationId + } + properties: { + host: endpoint.host + // Apply default audience for SystemAssignedManagedIdentity when not specified + #disable-next-line BCP225 + authentication: endpoint.authentication.method == 'SystemAssignedManagedIdentity' + ? { + method: 'SystemAssignedManagedIdentity' + systemAssignedManagedIdentitySettings: { + audience: endpoint.authentication.systemAssignedManagedIdentitySettings.?audience ?? defaultSystemAssignedManagedIdentityAudience + } + } + : endpoint.authentication + } + } +] + +// Existing ACR references for role assignments +resource acrs 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = [ + for endpoint in endpointsWithAcr: { + name: last(split(endpoint.acrResourceId!, '/'))! + } +] + +// ACR role assignments for SystemAssignedManagedIdentity endpoints with acrResourceId +@batchSize(1) +resource acrPullRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for (endpoint, i) in endpointsWithAcr: { + name: guid(endpoint.acrResourceId!, extensionPrincipalId, 'AcrPull', endpoint.name) + scope: acrs[i] + properties: { + // https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/containers#acrpull + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7f951dda-4ed3-4680-a7ca-43fe172d538d' + ) + principalId: extensionPrincipalId + principalType: 'ServicePrincipal' + } + } +] + +/* + Outputs +*/ + +@description('Default MCR registry endpoint.') +output mcrEndpoint object = { + id: mcrEndpoint.id + name: mcrEndpoint.name + host: 'mcr.microsoft.com' +} + +@description('Array of custom registry endpoints.') +output customEndpointsOutput array = [ + for (endpoint, i) in registryEndpoints: { + id: customEndpoints[i].id + name: customEndpoints[i].name + host: endpoint.host + } +] + +@description('Array of ACR role assignment IDs.') +output acrRoleAssignments array = [ + for (endpoint, i) in endpointsWithAcr: { + name: endpoint.name + id: acrPullRoleAssignments[i].id + } +] diff --git a/src/100-edge/110-iot-ops/bicep/types.bicep b/src/100-edge/110-iot-ops/bicep/types.bicep index 8a49929f..a4415082 100644 --- a/src/100-edge/110-iot-ops/bicep/types.bicep +++ b/src/100-edge/110-iot-ops/bicep/types.bicep @@ -643,3 +643,101 @@ type AkriConnectorTemplate = { @description('Trust settings configuration.') trustSettings: AkriTrustSettings? } + +// ============================================================================ +// Registry Endpoint Types +// ============================================================================ + +@export() +@description('Authentication settings for System-Assigned Managed Identity.') +type SystemAssignedManagedIdentitySettings = { + @description('Audience of the service to authenticate against. Defaults to "https://management.azure.com/" for ACR.') + audience: string? +} + +@export() +@description('Authentication settings for User-Assigned Managed Identity.') +type UserAssignedManagedIdentitySettings = { + @description('Client ID for the user-assigned managed identity.') + clientId: string + + @description('Tenant ID where the managed identity is located.') + tenantId: string + + @description('Resource identifier (application ID URI) with .default suffix.') + scope: string? +} + +@export() +@description('Authentication settings for Artifact Pull Secret.') +type ArtifactPullSecretSettings = { + @description('The name of the kubernetes secret that contains the artifact pull secret.') + secretRef: string +} + +@export() +@description('Authentication configuration for a registry endpoint.') +@discriminator('method') +type RegistryAuthentication = + | RegistryAuthSystemAssignedManagedIdentity + | RegistryAuthUserAssignedManagedIdentity + | RegistryAuthArtifactPullSecret + | RegistryAuthAnonymous + +@export() +@description('System-Assigned Managed Identity authentication for registry endpoint.') +type RegistryAuthSystemAssignedManagedIdentity = { + @description('Authentication method.') + method: 'SystemAssignedManagedIdentity' + + @description('System-assigned managed identity settings.') + systemAssignedManagedIdentitySettings: SystemAssignedManagedIdentitySettings +} + +@export() +@description('User-Assigned Managed Identity authentication for registry endpoint.') +type RegistryAuthUserAssignedManagedIdentity = { + @description('Authentication method.') + method: 'UserAssignedManagedIdentity' + + @description('User-assigned managed identity settings.') + userAssignedManagedIdentitySettings: UserAssignedManagedIdentitySettings +} + +@export() +@description('Artifact Pull Secret authentication for registry endpoint.') +type RegistryAuthArtifactPullSecret = { + @description('Authentication method.') + method: 'ArtifactPullSecret' + + @description('Artifact pull secret settings.') + artifactPullSecretSettings: ArtifactPullSecretSettings +} + +@export() +@description('Anonymous authentication for registry endpoint.') +type RegistryAuthAnonymous = { + @description('Authentication method.') + method: 'Anonymous' + + @description('Anonymous authentication settings (empty object).') + anonymousSettings: object? +} + +@export() +@description('Container registry endpoint configuration for AIO instance.') +type RegistryEndpointConfig = { + @description('Unique name for the registry endpoint (3-63 chars, lowercase alphanumeric and hyphens).') + @minLength(3) + @maxLength(63) + name: string + + @description('Container registry hostname (e.g., myregistry.azurecr.io).') + host: string + + @description('Optional ACR resource ID for automatic AcrPull role assignment. Only applicable when authentication.method is SystemAssignedManagedIdentity.') + acrResourceId: string? + + @description('Authentication configuration for the registry.') + authentication: RegistryAuthentication +} diff --git a/src/100-edge/110-iot-ops/terraform/README.md b/src/100-edge/110-iot-ops/terraform/README.md index 93d1c777..362ea540 100644 --- a/src/100-edge/110-iot-ops/terraform/README.md +++ b/src/100-edge/110-iot-ops/terraform/README.md @@ -25,6 +25,7 @@ Instance can be created, and after. | iot\_ops\_init | ./modules/iot-ops-init | n/a | | iot\_ops\_instance | ./modules/iot-ops-instance | n/a | | opc\_ua\_simulator | ./modules/opc-ua-simulator | n/a | +| registry\_endpoints | ./modules/registry-endpoints | n/a | | role\_assignments | ./modules/role-assignment | n/a | ## Inputs @@ -51,6 +52,7 @@ Instance can be created, and after. | mqtt\_broker\_config | n/a | ```object({ brokerListenerServiceName = string brokerListenerPort = number serviceAccountAudience = string frontendReplicas = number frontendWorkers = number backendRedundancyFactor = number backendWorkers = number backendPartitions = number memoryProfile = string serviceType = string logsLevel = optional(string, "info") })``` | ```{ "backendPartitions": 2, "backendRedundancyFactor": 2, "backendWorkers": 2, "brokerListenerPort": 18883, "brokerListenerServiceName": "aio-broker", "frontendReplicas": 2, "frontendWorkers": 2, "logsLevel": "info", "memoryProfile": "Medium", "serviceAccountAudience": "aio-internal", "serviceType": "ClusterIp" }``` | no | | mqtt\_broker\_persistence\_config | Broker persistence configuration for disk-backed message storage | ```object({ enabled = bool max_size = string encryption_enabled = optional(bool) # Dynamic Settings dynamic_settings = optional(object({ user_property_key = string user_property_value = string })) # Retention Policy retain_policy = optional(object({ mode = string # "All", "None", "Custom" custom_settings = optional(object({ topics = optional(list(string)) dynamic_enabled = optional(bool) })) })) # State Store Policy state_store_policy = optional(object({ mode = string # "All", "None", "Custom" custom_settings = optional(object({ state_store_resources = optional(list(object({ key_type = string # "Pattern", "String", "Binary" keys = list(string) }))) dynamic_enabled = optional(bool) })) })) # Subscriber Queue Policy subscriber_queue_policy = optional(object({ mode = string # "All", "None", "Custom" custom_settings = optional(object({ subscriber_client_ids = optional(list(string)) topics = optional(list(string)) dynamic_enabled = optional(bool) })) })) # Persistent Volume Claim Specification persistent_volume_claim_spec = optional(object({ storage_class_name = optional(string) access_modes = optional(list(string)) volume_mode = optional(string) volume_name = optional(string) resources = optional(object({ requests = optional(map(string)) limits = optional(map(string)) })) data_source = optional(object({ api_group = optional(string) kind = string name = string })) selector = optional(object({ match_labels = optional(map(string)) match_expressions = optional(list(object({ key = string operator = string values = list(string) }))) })) })) })``` | `null` | no | | operations\_config | n/a | ```object({ namespace = string kubernetesDistro = string version = string train = string agentOperationTimeoutInMinutes = number })``` | ```{ "agentOperationTimeoutInMinutes": 120, "kubernetesDistro": "K3s", "namespace": "azure-iot-operations", "train": "stable", "version": "1.2.154" }``` | no | +| registry\_endpoints | List of additional container registry endpoints for pulling custom artifacts (WASM modules, graph definitions, connector templates). MCR (mcr.microsoft.com) is always added automatically with anonymous authentication. The `acr_resource_id` field enables automatic AcrPull role assignment for ACR endpoints using SystemAssignedManagedIdentity authentication. When `should_assign_acr_pull_for_aio` is true and `acr_resource_id` is provided, the AIO extension's identity will be granted AcrPull access to the specified ACR. | ```list(object({ name = string host = string acr_resource_id = optional(string) should_assign_acr_pull_for_aio = optional(bool, false) authentication = object({ method = string system_assigned_managed_identity_settings = optional(object({ audience = optional(string, "https://management.azure.com/") })) user_assigned_managed_identity_settings = optional(object({ client_id = string tenant_id = string scope = optional(string) })) artifact_pull_secret_settings = optional(object({ secret_ref = string })) }) }))``` | `[]` | no | | secret\_sync\_controller | n/a | ```object({ version = string train = string })``` | ```{ "train": "stable", "version": "1.1.5" }``` | no | | should\_assign\_key\_vault\_roles | Whether to assign Key Vault roles to provided Secret Sync identity. | `bool` | `true` | no | | should\_create\_anonymous\_broker\_listener | Whether to enable an insecure anonymous AIO MQ Broker Listener. Should only be used for dev or test environments | `bool` | `false` | no | @@ -64,13 +66,16 @@ Instance can be created, and after. ## Outputs -| Name | Description | -|----------------------------------|----------------------------------------------------------------------------------------------------------------| -| aio\_broker\_listener\_anonymous | The anonymous MQTT Broker Listener configuration details. | -| aio\_dataflow\_profile | The Azure IoT Operations dataflow profile. | -| aio\_instance | The Azure IoT Operations instance. | -| aio\_mqtt\_broker | The MQTT Broker configuration details. | -| aio\_namespace | The Azure IoT Operations namespace. | -| akri\_connector\_templates | Map of deployed Akri connector templates by name with id and type. Returns null if no connectors are deployed. | -| custom\_locations | The custom location details. | +| Name | Description | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------| +| aio\_broker\_listener\_anonymous | The anonymous MQTT Broker Listener configuration details. | +| aio\_dataflow\_profile | The Azure IoT Operations dataflow profile. | +| aio\_instance | The Azure IoT Operations instance. | +| aio\_mqtt\_broker | The MQTT Broker configuration details. | +| aio\_namespace | The Azure IoT Operations namespace. | +| akri\_connector\_templates | Map of deployed Akri connector templates by name with id and type. Returns null if no connectors are deployed. | +| custom\_locations | The custom location details. | +| registry\_endpoint\_mcr | The default MCR registry endpoint. | +| registry\_endpoints\_acr\_role\_assignments | Map of ACR role assignments by endpoint name. | +| registry\_endpoints\_custom | Map of custom registry endpoints by name with id, name, and host. | diff --git a/src/100-edge/110-iot-ops/terraform/main.tf b/src/100-edge/110-iot-ops/terraform/main.tf index 8255b86a..b81c7bd5 100644 --- a/src/100-edge/110-iot-ops/terraform/main.tf +++ b/src/100-edge/110-iot-ops/terraform/main.tf @@ -207,3 +207,17 @@ module "akri_connectors" { ca_configmap = "azure-iot-operations-aio-ca-trust-bundle" } } + +/* + * Registry Endpoints + */ + +module "registry_endpoints" { + source = "./modules/registry-endpoints" + depends_on = [module.iot_ops_instance] + + aio_instance_id = module.iot_ops_instance.aio_instance.id + custom_location_id = module.iot_ops_instance.custom_locations.id + extension_principal_id = module.iot_ops_instance.extension_identity.principal_id + registry_endpoints = var.registry_endpoints +} diff --git a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/README.md b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/README.md index 2511759b..98687a45 100644 --- a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/README.md +++ b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/README.md @@ -31,7 +31,6 @@ Deploys an AIO instance. | [azapi_resource.data_profiles](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource | | [azapi_resource.default_aio_keyvault_secret_provider_class](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource | | [azapi_resource.instance](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource | -| [azapi_resource.registry_endpoint](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource | | [azapi_update_resource.aio_instance_secret_sync_update](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/update_resource) | resource | | [azurerm_arc_kubernetes_cluster_extension.iot_operations](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/arc_kubernetes_cluster_extension) | resource | | [azurerm_role_assignment.schema_registry](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | @@ -68,12 +67,13 @@ Deploys an AIO instance. ## Outputs -| Name | Description | -|----------------------------------|-------------------------------------------------------| -| aio\_broker\_listener\_anonymous | Anonymous MQTT Broker Listener configuration details. | -| aio\_dataflow\_profile | Azure IoT Operations dataflow profile details. | -| aio\_instance | Azure IoT Operations instance details. | -| aio\_mqtt\_broker | MQTT Broker configuration details. | -| aio\_namespace | Azure IoT Operations namespace. | -| custom\_locations | Custom location details. | +| Name | Description | +|----------------------------------|--------------------------------------------------------------| +| aio\_broker\_listener\_anonymous | Anonymous MQTT Broker Listener configuration details. | +| aio\_dataflow\_profile | Azure IoT Operations dataflow profile details. | +| aio\_instance | Azure IoT Operations instance details. | +| aio\_mqtt\_broker | MQTT Broker configuration details. | +| aio\_namespace | Azure IoT Operations namespace. | +| custom\_locations | Custom location details. | +| extension\_identity | AIO Arc extension identity information for role assignments. | diff --git a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf index d051dc2e..b916e74d 100644 --- a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf +++ b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/main.tf @@ -171,29 +171,6 @@ resource "azapi_resource" "instance" { schema_validation_enabled = false # Disable schema validation for azapi_resource for 2025-10-01 until azapi provider supports it } -resource "azapi_resource" "registry_endpoint" { - type = "Microsoft.IoTOperations/instances/registryEndpoints@2025-10-01" - name = "default" - parent_id = azapi_resource.instance.id - - body = { - extendedLocation = { - type = "CustomLocation" - name = azapi_resource.custom_location.id - } - properties = { - host = "mcr.microsoft.com" - authentication = { - method = "Anonymous" - anonymousSettings = {} - } - } - } - - response_export_values = ["name", "id"] - schema_validation_enabled = false -} - resource "azapi_resource" "broker" { type = "Microsoft.IoTOperations/instances/brokers@2025-10-01" name = "default" diff --git a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/outputs.tf b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/outputs.tf index 3cf70b3a..26e85a9a 100644 --- a/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/outputs.tf +++ b/src/100-edge/110-iot-ops/terraform/modules/iot-ops-instance/outputs.tf @@ -35,6 +35,17 @@ output "custom_locations" { } } +/* + * Extension Identity Outputs + */ + +output "extension_identity" { + description = "AIO Arc extension identity information for role assignments." + value = { + principal_id = azurerm_arc_kubernetes_cluster_extension.iot_operations.identity[0].principal_id + } +} + /* * MQTT Broker Outputs */ diff --git a/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/README.md b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/README.md new file mode 100644 index 00000000..a9464093 --- /dev/null +++ b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/README.md @@ -0,0 +1,46 @@ + +# Registry Endpoints + +Manages container registry endpoints for Azure IoT Operations, including the default +MCR endpoint and custom registry endpoints with optional ACR role assignments. + +## Requirements + +| Name | Version | +|-----------|-----------------| +| terraform | >= 1.9.8, < 2.0 | +| azapi | >= 2.0 | +| azurerm | >= 4.0 | + +## Providers + +| Name | Version | +|---------|---------| +| azapi | >= 2.0 | +| azurerm | >= 4.0 | + +## Resources + +| Name | Type | +|----------------------------------------------------------------------------------------------------------------------------------------------|----------| +| [azapi_resource.registry_endpoint](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource | +| [azapi_resource.registry_endpoint_mcr](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource) | resource | +| [azurerm_role_assignment.registry_acr_pull](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|--------------------------|-------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|:--------:| +| aio\_instance\_id | Azure IoT Operations instance ID (parent for registry endpoints) | `string` | n/a | yes | +| custom\_location\_id | Custom location ID for the Azure IoT Operations deployment | `string` | n/a | yes | +| extension\_principal\_id | Principal ID of the AIO Arc extension's system-assigned identity for ACR role assignments | `string` | n/a | yes | +| registry\_endpoints | List of custom registry endpoints to configure. | ```list(object({ name = string host = string acr_resource_id = optional(string) should_assign_acr_pull_for_aio = optional(bool, false) authentication = object({ method = string system_assigned_managed_identity_settings = optional(object({ audience = optional(string, "https://management.azure.com/") })) user_assigned_managed_identity_settings = optional(object({ client_id = string tenant_id = string scope = optional(string) })) artifact_pull_secret_settings = optional(object({ secret_ref = string })) }) }))``` | n/a | yes | + +## Outputs + +| Name | Description | +|------------------------|-------------------------------------------------| +| acr\_role\_assignments | Map of ACR role assignment IDs by endpoint name | +| custom\_endpoints | Map of custom registry endpoints by name | +| mcr\_endpoint | Default MCR registry endpoint | + diff --git a/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/main.tf b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/main.tf new file mode 100644 index 00000000..03861437 --- /dev/null +++ b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/main.tf @@ -0,0 +1,89 @@ +/** + * # Registry Endpoints + * + * Manages container registry endpoints for Azure IoT Operations, including the default + * MCR endpoint and custom registry endpoints with optional ACR role assignments. + */ + +// Default MCR endpoint (always created) +resource "azapi_resource" "registry_endpoint_mcr" { + type = "Microsoft.IoTOperations/instances/registryEndpoints@2025-10-01" + name = "mcr" + parent_id = var.aio_instance_id + + body = { + extendedLocation = { + type = "CustomLocation" + name = var.custom_location_id + } + properties = { + host = "mcr.microsoft.com" + authentication = { + method = "Anonymous" + anonymousSettings = {} + } + } + } + + response_export_values = ["name", "id"] + schema_validation_enabled = false +} + +// Custom registry endpoints +resource "azapi_resource" "registry_endpoint" { + for_each = { for endpoint in var.registry_endpoints : endpoint.name => endpoint } + type = "Microsoft.IoTOperations/instances/registryEndpoints@2025-10-01" + name = each.value.name + parent_id = var.aio_instance_id + + body = { + extendedLocation = { + type = "CustomLocation" + name = var.custom_location_id + } + properties = { + host = each.value.host + authentication = merge( + { method = each.value.authentication.method }, + each.value.authentication.method == "SystemAssignedManagedIdentity" ? { + systemAssignedManagedIdentitySettings = each.value.authentication.system_assigned_managed_identity_settings != null ? { + audience = coalesce(each.value.authentication.system_assigned_managed_identity_settings.audience, "https://management.azure.com/") + } : { + audience = "https://management.azure.com/" + } + } : {}, + each.value.authentication.method == "UserAssignedManagedIdentity" ? { + userAssignedManagedIdentitySettings = { + clientId = each.value.authentication.user_assigned_managed_identity_settings.client_id + tenantId = each.value.authentication.user_assigned_managed_identity_settings.tenant_id + scope = each.value.authentication.user_assigned_managed_identity_settings.scope + } + } : {}, + each.value.authentication.method == "ArtifactPullSecret" ? { + artifactPullSecretSettings = { + secretRef = each.value.authentication.artifact_pull_secret_settings.secret_ref + } + } : {}, + each.value.authentication.method == "Anonymous" ? { + anonymousSettings = {} + } : {} + ) + } + } + + response_export_values = ["name", "id"] + schema_validation_enabled = false +} + +// ACR role assignments for SystemAssignedManagedIdentity endpoints with should_assign_acr_pull_for_aio enabled +resource "azurerm_role_assignment" "registry_acr_pull" { + for_each = { + for endpoint in var.registry_endpoints : endpoint.name => endpoint + if endpoint.authentication.method == "SystemAssignedManagedIdentity" && endpoint.should_assign_acr_pull_for_aio + } + + scope = each.value.acr_resource_id + role_definition_name = "AcrPull" + principal_id = var.extension_principal_id + skip_service_principal_aad_check = true +} diff --git a/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/outputs.tf b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/outputs.tf new file mode 100644 index 00000000..05632e1d --- /dev/null +++ b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/outputs.tf @@ -0,0 +1,31 @@ +/* + * Registry Endpoint Outputs + */ + +output "mcr_endpoint" { + description = "Default MCR registry endpoint" + value = { + id = azapi_resource.registry_endpoint_mcr.id + name = azapi_resource.registry_endpoint_mcr.name + host = "mcr.microsoft.com" + } +} + +output "custom_endpoints" { + description = "Map of custom registry endpoints by name" + value = { + for name, ep in azapi_resource.registry_endpoint : name => { + id = ep.id + name = ep.name + host = var.registry_endpoints[index(var.registry_endpoints[*].name, name)].host + } + } +} + +output "acr_role_assignments" { + description = "Map of ACR role assignment IDs by endpoint name" + value = { + for name, ra in azurerm_role_assignment.registry_acr_pull : + name => ra.id + } +} diff --git a/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/variables.tf b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/variables.tf new file mode 100644 index 00000000..163a78fc --- /dev/null +++ b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/variables.tf @@ -0,0 +1,46 @@ +/* + * Required Dependencies + */ + +variable "aio_instance_id" { + type = string + description = "Azure IoT Operations instance ID (parent for registry endpoints)" +} + +variable "custom_location_id" { + type = string + description = "Custom location ID for the Azure IoT Operations deployment" +} + +variable "extension_principal_id" { + type = string + description = "Principal ID of the AIO Arc extension's system-assigned identity for ACR role assignments" +} + +/* + * Registry Endpoints Configuration + */ + +variable "registry_endpoints" { + type = list(object({ + name = string + host = string + acr_resource_id = optional(string) + should_assign_acr_pull_for_aio = optional(bool, false) + authentication = object({ + method = string + system_assigned_managed_identity_settings = optional(object({ + audience = optional(string, "https://management.azure.com/") + })) + user_assigned_managed_identity_settings = optional(object({ + client_id = string + tenant_id = string + scope = optional(string) + })) + artifact_pull_secret_settings = optional(object({ + secret_ref = string + })) + }) + })) + description = "List of custom registry endpoints to configure." +} diff --git a/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/versions.tf b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/versions.tf new file mode 100644 index 00000000..3e6fc839 --- /dev/null +++ b/src/100-edge/110-iot-ops/terraform/modules/registry-endpoints/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.9.8, < 2.0" + required_providers { + azapi = { + source = "Azure/azapi" + version = ">= 2.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.0" + } + } +} diff --git a/src/100-edge/110-iot-ops/terraform/outputs.tf b/src/100-edge/110-iot-ops/terraform/outputs.tf index e7b787aa..4ff7ecf9 100644 --- a/src/100-edge/110-iot-ops/terraform/outputs.tf +++ b/src/100-edge/110-iot-ops/terraform/outputs.tf @@ -53,3 +53,22 @@ output "akri_connector_templates" { length(var.custom_akri_connectors) > 0 ]) ? module.akri_connectors[0].connector_templates : null } + +/* + * Registry Endpoints Outputs + */ + +output "registry_endpoint_mcr" { + description = "The default MCR registry endpoint." + value = module.registry_endpoints.mcr_endpoint +} + +output "registry_endpoints_custom" { + description = "Map of custom registry endpoints by name with id, name, and host." + value = module.registry_endpoints.custom_endpoints +} + +output "registry_endpoints_acr_role_assignments" { + description = "Map of ACR role assignments by endpoint name." + value = module.registry_endpoints.acr_role_assignments +} diff --git a/src/100-edge/110-iot-ops/terraform/tests/iot-ops.tftest.hcl b/src/100-edge/110-iot-ops/terraform/tests/iot-ops.tftest.hcl index b189f30b..4dc8ffd6 100644 --- a/src/100-edge/110-iot-ops/terraform/tests/iot-ops.tftest.hcl +++ b/src/100-edge/110-iot-ops/terraform/tests/iot-ops.tftest.hcl @@ -753,3 +753,231 @@ run "create_with_additional_cluster_extension_ids" { error_message = "Should contain container storage extension ID" } } + +# Test default empty registry_endpoints array (MCR endpoint always created) +run "create_with_default_registry_endpoints" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + arc_connected_cluster = run.setup_tests.arc_connected_cluster + } + + assert { + condition = length(var.registry_endpoints) == 0 + error_message = "Default registry_endpoints should be empty array" + } +} + +# Test single ACR endpoint with SystemAssignedManagedIdentity +run "create_with_acr_registry_endpoint" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + arc_connected_cluster = run.setup_tests.arc_connected_cluster + registry_endpoints = [ + { + name = "myacr" + host = "myregistry.azurecr.io" + authentication = { + method = "SystemAssignedManagedIdentity" + system_assigned_managed_identity_settings = { + audience = "https://management.azure.com/" + } + } + } + ] + } + + assert { + condition = length(var.registry_endpoints) == 1 + error_message = "Should have one custom registry endpoint" + } + + assert { + condition = var.registry_endpoints[0].name == "myacr" + error_message = "Registry endpoint name should be 'myacr'" + } + + assert { + condition = var.registry_endpoints[0].authentication.method == "SystemAssignedManagedIdentity" + error_message = "Authentication method should be SystemAssignedManagedIdentity" + } +} + +# Test multiple endpoints with different authentication methods +run "create_with_multiple_registry_endpoints" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + arc_connected_cluster = run.setup_tests.arc_connected_cluster + registry_endpoints = [ + { + name = "company-acr" + host = "contoso.azurecr.io" + authentication = { + method = "SystemAssignedManagedIdentity" + } + }, + { + name = "dockerhub" + host = "docker.io" + authentication = { + method = "ArtifactPullSecret" + artifact_pull_secret_settings = { + secret_ref = "dockerhub-creds" + } + } + } + ] + } + + assert { + condition = length(var.registry_endpoints) == 2 + error_message = "Should have two custom registry endpoints" + } + + assert { + condition = var.registry_endpoints[0].authentication.method == "SystemAssignedManagedIdentity" + error_message = "First endpoint should use SystemAssignedManagedIdentity" + } + + assert { + condition = var.registry_endpoints[1].authentication.method == "ArtifactPullSecret" + error_message = "Second endpoint should use ArtifactPullSecret" + } +} + +# Test ACR endpoint with acr_resource_id for automatic AcrPull role assignment +run "create_with_acr_registry_endpoint_and_role_assignment" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + arc_connected_cluster = run.setup_tests.arc_connected_cluster + registry_endpoints = [ + { + name = "myacr" + host = "myregistry.azurecr.io" + acr_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ContainerRegistry/registries/myregistry" + should_assign_acr_pull_for_aio = true + authentication = { + method = "SystemAssignedManagedIdentity" + system_assigned_managed_identity_settings = { + audience = "https://management.azure.com/" + } + } + } + ] + } + + assert { + condition = length(var.registry_endpoints) == 1 + error_message = "Should have one registry endpoint" + } + + assert { + condition = var.registry_endpoints[0].acr_resource_id != null + error_message = "Registry endpoint should have acr_resource_id configured" + } + + assert { + condition = var.registry_endpoints[0].should_assign_acr_pull_for_aio == true + error_message = "should_assign_acr_pull_for_aio should be true for AIO extension ACR access" + } + + assert { + condition = var.registry_endpoints[0].authentication.method == "SystemAssignedManagedIdentity" + error_message = "Authentication method should be SystemAssignedManagedIdentity when using acr_resource_id" + } +} + +# Test multiple endpoints with mixed acr_resource_id usage +run "create_with_mixed_acr_registry_endpoints" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + arc_connected_cluster = run.setup_tests.arc_connected_cluster + registry_endpoints = [ + { + name = "primary-acr" + host = "primary.azurecr.io" + acr_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ContainerRegistry/registries/primary" + should_assign_acr_pull_for_aio = true + authentication = { + method = "SystemAssignedManagedIdentity" + } + }, + { + name = "secondary-acr" + host = "secondary.azurecr.io" + authentication = { + method = "SystemAssignedManagedIdentity" + } + }, + { + name = "private-registry" + host = "private.example.com" + authentication = { + method = "ArtifactPullSecret" + artifact_pull_secret_settings = { + secret_ref = "private-registry-creds" + } + } + } + ] + } + + assert { + condition = length(var.registry_endpoints) == 3 + error_message = "Should have three registry endpoints" + } + + assert { + condition = var.registry_endpoints[0].acr_resource_id != null + error_message = "First endpoint should have acr_resource_id for automatic role assignment" + } + + assert { + condition = var.registry_endpoints[0].should_assign_acr_pull_for_aio == true + error_message = "First endpoint should have should_assign_acr_pull_for_aio enabled" + } + + assert { + condition = var.registry_endpoints[1].should_assign_acr_pull_for_aio == false + error_message = "Second endpoint should default to should_assign_acr_pull_for_aio = false when omitted" + } + + assert { + condition = var.registry_endpoints[1].acr_resource_id == null + error_message = "Second endpoint should not have acr_resource_id" + } + + assert { + condition = var.registry_endpoints[2].acr_resource_id == null + error_message = "Third endpoint should not have acr_resource_id" + } +} diff --git a/src/100-edge/110-iot-ops/terraform/tests/variable_validation.tftest.hcl b/src/100-edge/110-iot-ops/terraform/tests/variable_validation.tftest.hcl index 23f914b4..c355dae9 100644 --- a/src/100-edge/110-iot-ops/terraform/tests/variable_validation.tftest.hcl +++ b/src/100-edge/110-iot-ops/terraform/tests/variable_validation.tftest.hcl @@ -1037,3 +1037,238 @@ run "test__custom_akri_connectors__valid_custom_configuration" { } # This should pass validation } + +# Test cases for registry_endpoints validation + +# Test registry endpoint with reserved name 'mcr' +run "test__registry_endpoints__error_with_reserved_name_mcr" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "mcr" # Reserved name + host = "custom.azurecr.io" + authentication = { + method = "SystemAssignedManagedIdentity" + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with reserved name 'default' +run "test__registry_endpoints__error_with_reserved_name_default" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "default" # Reserved name + host = "custom.azurecr.io" + authentication = { + method = "SystemAssignedManagedIdentity" + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with invalid name format (uppercase) +run "test__registry_endpoints__error_with_invalid_name_uppercase" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "MyACR" # Invalid: contains uppercase + host = "myacr.azurecr.io" + authentication = { + method = "SystemAssignedManagedIdentity" + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with invalid name format (too short) +run "test__registry_endpoints__error_with_invalid_name_too_short" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "ab" # Invalid: less than 3 characters + host = "myacr.azurecr.io" + authentication = { + method = "SystemAssignedManagedIdentity" + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with invalid authentication method +run "test__registry_endpoints__error_with_invalid_auth_method" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "myacr" + host = "myacr.azurecr.io" + authentication = { + method = "InvalidMethod" # Invalid authentication method + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with acr_resource_id on non-SystemAssignedManagedIdentity auth +run "test__registry_endpoints__error_with_acr_resource_id_wrong_auth" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "myacr" + host = "myacr.azurecr.io" + acr_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ContainerRegistry/registries/myacr" + authentication = { + method = "ArtifactPullSecret" # acr_resource_id requires SystemAssignedManagedIdentity + artifact_pull_secret_settings = { + secret_ref = "acr-creds" + } + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with UserAssignedManagedIdentity missing required settings +run "test__registry_endpoints__error_with_uami_missing_settings" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "myacr" + host = "myacr.azurecr.io" + authentication = { + method = "UserAssignedManagedIdentity" + # Missing required user_assigned_managed_identity_settings + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with ArtifactPullSecret missing required settings +run "test__registry_endpoints__error_with_artifact_secret_missing_settings" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "myregistry" + host = "myregistry.example.com" + authentication = { + method = "ArtifactPullSecret" + # Missing required artifact_pull_secret_settings + } + } + ] + } + expect_failures = [var.registry_endpoints] +} + +# Test registry endpoint with valid SystemAssignedManagedIdentity and acr_resource_id +run "test__registry_endpoints__valid_with_acr_resource_id" { + command = plan + variables { + resource_group = run.setup_tests.aio_resource_group + secret_sync_key_vault = run.setup_tests.sse_key_vault + secret_sync_identity = run.setup_tests.sse_user_assigned_identity + aio_identity = run.setup_tests.aio_user_assigned_identity + arc_connected_cluster = run.setup_tests.arc_connected_cluster + adr_schema_registry = run.setup_tests.adr_schema_registry + adr_namespace = run.setup_tests.adr_namespace + + registry_endpoints = [ + { + name = "myacr" + host = "myacr.azurecr.io" + acr_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ContainerRegistry/registries/myacr" + should_assign_acr_pull_for_aio = true + authentication = { + method = "SystemAssignedManagedIdentity" + } + } + ] + } + # This should pass validation +} diff --git a/src/100-edge/110-iot-ops/terraform/variables.tf b/src/100-edge/110-iot-ops/terraform/variables.tf index 2ee7297f..7dd1d27f 100644 --- a/src/100-edge/110-iot-ops/terraform/variables.tf +++ b/src/100-edge/110-iot-ops/terraform/variables.tf @@ -71,7 +71,90 @@ variable "should_assign_key_vault_roles" { default = true } -/* - * Required Variables - */ +variable "registry_endpoints" { + type = list(object({ + name = string + host = string + acr_resource_id = optional(string) + should_assign_acr_pull_for_aio = optional(bool, false) + authentication = object({ + method = string + system_assigned_managed_identity_settings = optional(object({ + audience = optional(string, "https://management.azure.com/") + })) + user_assigned_managed_identity_settings = optional(object({ + client_id = string + tenant_id = string + scope = optional(string) + })) + artifact_pull_secret_settings = optional(object({ + secret_ref = string + })) + }) + })) + + default = [] + description = <<-EOT + List of additional container registry endpoints for pulling custom artifacts (WASM modules, graph definitions, connector templates). + MCR (mcr.microsoft.com) is always added automatically with anonymous authentication. + + The `acr_resource_id` field enables automatic AcrPull role assignment for ACR endpoints + using SystemAssignedManagedIdentity authentication. When `should_assign_acr_pull_for_aio` is true + and `acr_resource_id` is provided, the AIO extension's identity will be granted AcrPull access to the specified ACR. + EOT + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + can(regex("^[a-z0-9][a-z0-9-]*[a-z0-9]$", ep.name)) && length(ep.name) >= 3 && length(ep.name) <= 63 + ]) + error_message = "Registry endpoint name must be 3-63 characters, contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + contains(["SystemAssignedManagedIdentity", "UserAssignedManagedIdentity", "ArtifactPullSecret", "Anonymous"], ep.authentication.method) + ]) + error_message = "Authentication method must be one of: SystemAssignedManagedIdentity, UserAssignedManagedIdentity, ArtifactPullSecret, Anonymous" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.authentication.method != "UserAssignedManagedIdentity" || ( + ep.authentication.user_assigned_managed_identity_settings != null && + ep.authentication.user_assigned_managed_identity_settings.client_id != null && + ep.authentication.user_assigned_managed_identity_settings.tenant_id != null + ) + ]) + error_message = "UserAssignedManagedIdentity authentication requires client_id and tenant_id in user_assigned_managed_identity_settings" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.authentication.method != "ArtifactPullSecret" || ( + ep.authentication.artifact_pull_secret_settings != null && + ep.authentication.artifact_pull_secret_settings.secret_ref != null + ) + ]) + error_message = "ArtifactPullSecret authentication requires secret_ref in artifact_pull_secret_settings" + } + + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.name != "mcr" && ep.name != "default" + ]) + error_message = "Registry endpoint names 'mcr' and 'default' are reserved" + } + validation { + condition = alltrue([ + for ep in var.registry_endpoints : + ep.acr_resource_id == null || ep.authentication.method == "SystemAssignedManagedIdentity" + ]) + error_message = "acr_resource_id can only be specified with SystemAssignedManagedIdentity authentication method" + } +} diff --git a/src/100-edge/111-assets/bicep/README.md b/src/100-edge/111-assets/bicep/README.md index bdb21fa2..2a3e6be0 100644 --- a/src/100-edge/111-assets/bicep/README.md +++ b/src/100-edge/111-assets/bicep/README.md @@ -7,18 +7,18 @@ Deploys Kubernetes asset definitions to a connected cluster using the namespaced ## Parameters -| Name | Description | Type | Default | Required | -|:-----------------------------------|:----------------------------------------------------------------------------------------------|:-----------------------------------|:--------|:---------| -| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | -| customLocationId | The ID (resource ID) of the custom location to retrieve. | `string` | n/a | yes | -| adrNamespaceName | Azure Device Registry namespace name to use with Azure IoT Operations. | `string` | n/a | yes | -| namespacedDevices | List of namespaced devices to create. | `array` | [] | no | -| assetEndpointProfiles | List of asset endpoint profiles to create. | `array` | [] | no | -| legacyAssets | List of legacy assets to create. | `array` | [] | no | -| namespacedAssets | List of namespaced assets to create. | `array` | [] | no | -| shouldCreateDefaultAsset | Whether to create a default legacy asset and endpoint profile. | `bool` | `false` | no | -| shouldCreateDefaultNamespacedAsset | Whether to create a default namespaced asset and device. | `bool` | `false` | no | -| k8sBridgePrincipalId | The principal ID of the K8 Bridge for Azure IoT Operations. Required for OPC asset discovery. | `string` | n/a | no | +| Name | Description | Type | Default | Required | +|:-----------------------------------|:----------------------------------------------------------------------------------------------|:---------------------------------------------------|:--------|:---------| +| common | The common component configuration. | `[_2.Common](#user-defined-types)` | n/a | yes | +| customLocationId | The ID (resource ID) of the custom location to retrieve. | `string` | n/a | yes | +| adrNamespaceName | Azure Device Registry namespace name to use with Azure IoT Operations. | `string` | n/a | yes | +| namespacedDevices | List of namespaced devices to create. | `[_1.NamespacedDevice](#user-defined-types)[]` | [] | no | +| assetEndpointProfiles | List of asset endpoint profiles to create. | `[_1.AssetEndpointProfile](#user-defined-types)[]` | [] | no | +| legacyAssets | List of legacy assets to create. | `[_1.LegacyAsset](#user-defined-types)[]` | [] | no | +| namespacedAssets | List of namespaced assets to create. | `[_1.NamespacedAsset](#user-defined-types)[]` | [] | no | +| shouldCreateDefaultAsset | Whether to create a default legacy asset and endpoint profile. | `bool` | `false` | no | +| shouldCreateDefaultNamespacedAsset | Whether to create a default namespaced asset and device. | `bool` | `false` | no | +| k8sBridgePrincipalId | The principal ID of the K8 Bridge for Azure IoT Operations. Required for OPC asset discovery. | `string` | n/a | no | ## Resources diff --git a/src/900-tools-utilities/904-test-utilities/README.md b/src/900-tools-utilities/904-test-utilities/README.md new file mode 100644 index 00000000..44283f44 --- /dev/null +++ b/src/900-tools-utilities/904-test-utilities/README.md @@ -0,0 +1,478 @@ +# Blueprint Test Utilities + +Shared testing utilities for all edge-ai blueprints providing reusable, framework-agnostic functions for validating and deploying infrastructure as code. + +## Package: `testutil` + +**Import path:** `github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities` + +**Purpose:** Standardized testing primitives for blueprint validation across both Terraform and Bicep implementations. + +## Key Features + +### 1. Contract Testing + +Static validation of IaC output declarations without deployment: + +- โšก **Fast** - Completes in seconds +- ๐Ÿ’ฐ **Zero Cost** - No Azure resources created +- ๐Ÿ”’ **Type Safe** - Enforces contract between test code and IaC +- ๐Ÿ› **Early Detection** - Catches drift before deployment + +### 2. Deployment Functions + +Unified deployment and output retrieval: + +- ๐Ÿ”„ **Framework Agnostic** - Consistent interface for Terraform and Bicep +- ๐Ÿงน **Automatic Cleanup** - Optional resource teardown after tests +- ๐Ÿ“Š **Output Normalization** - Converts outputs to common format +- ๐Ÿ”ง **Configuration Driven** - Environment variable support + +--- + +## Contract Testing + +Contract testing validates that all outputs required by test code are properly declared in the IaC configuration, preventing runtime failures during expensive deployments. + +### How It Works + +1. **Extract Declarations:** Parse IaC files to find declared outputs +2. **Compare Contracts:** Match declared outputs against required outputs +3. **Report Drift:** Fail fast if any required output is missing + +### Benefits + +- โšก **Fast** - Runs in seconds without Azure authentication +- ๐Ÿ’ฐ **Zero Cost** - No Azure resources created +- ๐Ÿ”’ **Type Safety** - Enforces contract between tests and IaC +- ๐Ÿ› **Early Detection** - Catches missing outputs before deployment +- ๐Ÿ”„ **CI/CD Ready** - Perfect for pre-deployment validation gates + +### Functions + +#### `GetTerraformDeclaredOutputs(t, terraformDir) []string` + +Extracts declared output names from Terraform configuration using `terraform-docs`. + +**Parameters:** + +- `t *testing.T` - Test context for error reporting +- `terraformDir string` - Absolute or relative path to directory containing `.tf` files + +**Returns:** List of output names declared in Terraform configuration + +**Requirements:** `terraform-docs` installed and in PATH, valid `.tf` files in directory + +**See:** [contract_terraform_test.go](../../../blueprints/full-single-node-cluster/tests/contract_terraform_test.go) for usage example + +#### `GetBicepDeclaredOutputs(t, bicepDir) []string` + +Extracts declared output names from Bicep configuration using `az bicep build`. + +**Parameters:** + +- `t *testing.T` - Test context for error reporting +- `bicepDir string` - Absolute or relative path to directory containing `main.bicep` + +**Returns:** List of output names declared in Bicep configuration + +**Requirements:** Azure CLI with Bicep installed, `main.bicep` file in directory + +**See:** [contract_bicep_test.go](../../../blueprints/full-single-node-cluster/tests/contract_bicep_test.go) for usage example + +#### `ValidateOutputContract(t, declaredOutputs, requiredOutputs, framework)` + +Validates that all required outputs are declared in IaC configuration. Fails test with detailed error reporting if any outputs are missing. + +**Parameters:** + +- `t *testing.T` - Test context for error reporting +- `declaredOutputs []string` - Outputs declared in IaC (from `GetTerraform/BicepDeclaredOutputs`) +- `requiredOutputs []string` - Outputs required by tests (from `BlueprintOutputs.GetRequiredOutputKeys()`) +- `framework string` - Framework name for error messages + +**Behavior:** Passes if all required outputs are declared, fails with detailed report if any are missing + +**See:** [contract_terraform_test.go](../../../blueprints/full-single-node-cluster/tests/contract_terraform_test.go) and [validation.go](../../../blueprints/full-single-node-cluster/tests/validation.go) for implementation + +#### `ValidateTerraformContract(t, terraformDir, requiredOutputs)` + +Convenience function combining `GetTerraformDeclaredOutputs` and `ValidateOutputContract`. + +**Parameters:** Test context, Terraform directory path, required output names list + +**Equivalent to:** Calling `GetTerraformDeclaredOutputs` followed by `ValidateOutputContract` + +**See:** [contract_terraform_test.go](../../../blueprints/full-single-node-cluster/tests/contract_terraform_test.go) for usage + +#### `ValidateBicepContract(t, bicepDir, requiredOutputs)` + +Convenience function combining `GetBicepDeclaredOutputs` and `ValidateOutputContract`. +Automatically converts required output names from snake_case to camelCase for Bicep comparison. + +**Parameters:** + +- `t *testing.T` - Test context for error reporting +- `bicepDir string` - Absolute or relative path to directory containing `main.bicep` +- `requiredOutputs []string` - Output names in snake_case (automatically converted to camelCase) + +**Naming Convention Conversion:** + +- `deployment_summary` โ†’ `deploymentSummary` +- `azure_iot_operations` โ†’ `azureIotOperations` +- `acr_network_posture` โ†’ `acrNetworkPosture` + +**Behavior:** Converts required outputs to camelCase, then validates against Bicep declarations + +**See:** [contract_bicep_test.go](../../../blueprints/full-single-node-cluster/tests/contract_bicep_test.go) for usage + +#### `SnakeToCamelCase(s string) string` + +Converts a snake_case string to camelCase. Used internally by `ValidateBicepContract` to transform output names from Terraform convention to Bicep convention. + +**Parameters:** `s string` - Input string in snake_case format + +**Returns:** String converted to camelCase + +**Examples:** + +- `"deployment_summary"` โ†’ `"deploymentSummary"` +- `"azure_iot_operations"` โ†’ `"azureIotOperations"` +- `"nat_gateway_public_ips"` โ†’ `"natGatewayPublicIps"` + +#### `CamelToSnakeCase(s string) string` + +Converts a camelCase string to snake_case. Used internally by `ParseBicepOutputsFromMap` to transform map keys from Bicep convention to Terraform convention. + +**Parameters:** `s string` - Input string in camelCase format + +**Returns:** String converted to snake_case + +**Examples:** + +- `"resourceGroup"` โ†’ `"resource_group"` +- `"keyVaultName"` โ†’ `"key_vault_name"` +- `"aioIdentity"` โ†’ `"aio_identity"` + +#### `GetOutputKeysFromStruct(outputStruct any) []string` + +Extracts all output names defined in a struct using reflection. Reads `output` tags from struct fields. + +**Parameters:** `outputStruct` - Struct instance (value or pointer) with `output` tags + +**Returns:** Slice of output names extracted from struct tags + +**Usage:** Called by `BlueprintOutputs.GetRequiredOutputKeys()` to get list of required outputs for contract validation. + +--- + +## Output Parsing Functions + +These functions convert raw deployment outputs to strongly-typed structs for use in validation code. + +### `ParseTerraformOutputsFromMap(t, raw, outputStruct)` + +Converts raw Terraform deployment outputs to a strongly-typed output struct using reflection. + +**Parameters:** + +- `t *testing.T` - Test context for error reporting +- `raw map[string]any` - Raw output map from `terraform.OutputAll()` +- `outputStruct any` - Pointer to struct with `output` tags + +**Behavior:** Maps outputs by matching `output` struct tag to raw map keys. Fails test if any required output is missing. + +### `ParseBicepOutputsFromMap(t, raw, outputStruct)` + +Converts raw Bicep deployment outputs to a strongly-typed output struct. + +**Parameters:** + +- `t *testing.T` - Test context for error reporting +- `raw map[string]any` - Raw output map from Bicep deployment +- `outputStruct any` - Pointer to struct with `output` tags (snake_case) + +**Key Differences from Terraform:** + +1. **Output name conversion:** Converts snake_case struct tags to camelCase for Bicep lookup + - Struct tag `output:"deployment_summary"` โ†’ looks up `deploymentSummary` in raw outputs +2. **Recursive map key transformation:** Converts all map keys from camelCase to snake_case + - Bicep returns `{"resourceGroup": "my-rg"}` โ†’ becomes `{"resource_group": "my-rg"}` + - This allows validation code to use consistent snake_case keys regardless of framework + +**Why This Matters:** Validation code can use the same key names (snake_case) for both Terraform and Bicep outputs: + +```go +// Works for both frameworks: +outputs.DeploymentSummary["resource_group"] +outputs.SecurityIdentity["key_vault_name"] +``` + +--- + +## Deployment Functions + +These functions handle deployment and output retrieval for both Terraform and Bicep. + +### Terraform Deployment + +#### `DeployTerraform(t, terraformDir, vars, shouldCleanup) map[string]any` + +Executes complete Terraform deployment workflow: init, apply, and optional destroy. + +**Parameters:** + +- `t *testing.T` - Test context +- `terraformDir string` - Path to directory containing `.tf` files +- `vars map[string]any` - Terraform input variables +- `shouldCleanup bool` - Register automatic cleanup via `t.Cleanup()` + +**Returns:** All Terraform outputs as map + +**Behavior:** Runs init/apply, retrieves outputs, optionally registers destroy cleanup handler + +**Requirements:** Terraform CLI in PATH, `ARM_SUBSCRIPTION_ID` set, Azure authenticated + +**Note:** Read `shouldCleanup` from `CLEANUP_RESOURCES` environment variable to avoid accidental deletion + +**See:** [deploy_terraform_test.go](../../../blueprints/full-single-node-cluster/tests/deploy_terraform_test.go) for complete usage pattern + +### Bicep Deployment + +#### DeployBicep + +**Signature:** + +```go +DeployBicep( + t *testing.T, + bicepDir string, + subscriptionID string, + deploymentName string, + resourceGroupName string, + params map[string]any, + shouldCleanup bool, +) map[string]any +``` + +Executes complete Bicep deployment workflow at subscription scope with optional resource group cleanup. + +**Parameters:** + +- `t *testing.T` - Test context for logging and error reporting +- `bicepDir string` - Path to directory containing `main.bicep` +- `subscriptionID string` - Azure subscription ID (get via `az account show --query id -o tsv`) +- `deploymentName string` - Unique deployment name (visible in Azure Portal deployments) +- `resourceGroupName string` - Resource group name to delete during cleanup +- `params map[string]any` - Bicep parameters as nested map structure +- `shouldCleanup bool` - If `true`, registers resource group deletion via `t.Cleanup()` + +**Returns:** `map[string]any` - All Bicep outputs converted to flat key-value pairs (Terraform-compatible format) + +**Behavior:** Writes temp parameters file, runs subscription-level deployment, retrieves and flattens outputs, optionally registers resource group deletion cleanup + +**Requirements:** Azure CLI authenticated, Bicep installed, subscription-level deployment permissions + +**Important Notes:** + +- Creates temporary `test-parameters.json` (auto-cleaned) +- Cleanup deletes **entire resource group** and all resources +- Converts Bicep output format to flat key-value pairs for compatibility + +**See:** [deploy_bicep_test.go](../../../blueprints/full-single-node-cluster/tests/deploy_bicep_test.go) for complete usage pattern + +#### `GetBicepOutputs(t, deploymentName) map[string]any` + +Retrieves outputs from an existing Bicep deployment without redeploying. Useful for validating existing infrastructure or skipping deployment in tests. + +**Parameters:** + +- `t *testing.T` - Test context for error reporting +- `deploymentName string` - Name of existing subscription-level deployment + +**Returns:** `map[string]any` - All deployment outputs in flattened format + +#### Skip Deployment Pattern + +Use `SKIP_BICEP_DEPLOYMENT=true` and `BICEP_DEPLOYMENT_NAME=` environment variables to query existing deployment instead of creating new one. + +**Requirements:** Azure CLI authenticated, deployment exists at subscription level, exact deployment name + +**See:** [deploy_bicep_test.go](../../../blueprints/full-single-node-cluster/tests/deploy_bicep_test.go) for conditional deployment pattern + +--- + +## Resource Cleanup Functions + +These functions handle resource teardown after testing. **Typically called automatically** via `t.Cleanup()` when `shouldCleanup=true` in deployment functions. + +### `DestroyTerraform(t, terraformOptions)` + +Executes `terraform destroy` to remove all resources in Terraform state. + +**Behavior:** Runs destroy with auto-approve, uses same variables as deployment, retries on transient failures + +**Typically called automatically** via `t.Cleanup()` when `DeployTerraform` has `shouldCleanup=true` + +### `DestroyBicep(t, subscriptionID, resourceGroupName)` + +Executes `az group delete` to remove resource group and all contained resources. + +**Behavior:** Deletes entire resource group asynchronously (no wait), more aggressive than Terraform destroy + +**Typically called automatically** via `t.Cleanup()` when `DeployBicep` has `shouldCleanup=true` + +**Important Notes:** + +- โš ๏ธ Bicep cleanup deletes entire resource group (all resources) +- โš ๏ธ No confirmation prompt when called +- ๐Ÿ”„ Automatic when using Deploy functions with `shouldCleanup=true` +- ๐Ÿงน Manual cleanup required if test fails before cleanup registration + +--- + +## Usage in Blueprint Tests + +The [full-single-node-cluster tests](../../../blueprints/full-single-node-cluster/tests/) directory provides a complete reference implementation. + +### Step 1: Define Blueprint Outputs + +Create `outputs.go` with struct using `tf` and `bicep` tags to map framework-specific output names. + +**See:** [outputs.go](../../../blueprints/full-single-node-cluster/tests/outputs.go) for struct definition pattern + +### Step 2: Create Contract Tests + +Create test files calling `ValidateTerraformContract` and `ValidateBicepContract` with required output keys. + +**See:** [contract_terraform_test.go](../../../blueprints/full-single-node-cluster/tests/contract_terraform_test.go) and [contract_bicep_test.go](../../../blueprints/full-single-node-cluster/tests/contract_bicep_test.go) + +### Step 3: Create Deployment Tests + +Create test files using `DeployTerraform` and `DeployBicep` with variables/parameters, then run validations. + +**See:** [deploy_terraform_test.go](../../../blueprints/full-single-node-cluster/tests/deploy_terraform_test.go) and [deploy_bicep_test.go](../../../blueprints/full-single-node-cluster/tests/deploy_bicep_test.go) + +--- + +## Dependencies + +### Required Tools + +For contract testing: + +- `terraform-docs` - Install: `brew install terraform-docs` +- `az bicep` - Install: `az bicep install` + +For deployment testing: + +- Terraform (for Terraform deployments) +- Azure CLI authenticated (`az login`) + +### Go Modules + +Add to your blueprint's `go.mod`: + +```go +require ( + github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities v0.0.0-00010101000000-000000000000 + github.com/gruntwork-io/terratest v0.54.0 + github.com/stretchr/testify v1.11.1 +) +``` + +--- + +## Best Practices + +### 1. Run Contract Tests First + +Run fast contract tests (`go test -run Contract`) before expensive deployment tests to catch configuration errors early. + +### 2. Control Resource Cleanup + +Tests preserve resources by default for inspection. Set `CLEANUP_RESOURCES=true` to enable automatic cleanup. + +### 3. Use Struct Tags for Output Mapping + +Define framework-specific output names using `tf` and `bicep` struct tags mapping to same field. + +**See:** [outputs.go](../../../blueprints/full-single-node-cluster/tests/outputs.go) for tag pattern + +### 4. Naming Convention Handling + +Use snake_case for output struct tags (matching Terraform convention). The test utilities automatically handle conversion for Bicep: + +- **Output names:** `ValidateBicepContract` converts snake_case tags to camelCase for lookup +- **Map keys:** `ParseBicepOutputsFromMap` recursively converts camelCase keys to snake_case + +This allows validation code to use consistent snake_case keys regardless of framework: + +```go +// Same validation code works for both Terraform and Bicep: +outputs.DeploymentSummary["resource_group"] // Terraform: resource_group, Bicep: resourceGroup โ†’ resource_group +outputs.SecurityIdentity["key_vault_name"] // Terraform: key_vault_name, Bicep: keyVaultName โ†’ key_vault_name +``` + +**Framework conventions:** + +- **Terraform outputs:** snake_case (e.g., `deployment_summary`) +- **Bicep outputs:** camelCase (e.g., `deploymentSummary`) +- **Struct tags:** Use snake_case, automatic conversion handles Bicep + +**See:** [outputs.tf](../../../blueprints/full-single-node-cluster/terraform/outputs.tf) and [main.bicep](../../../blueprints/full-single-node-cluster/bicep/main.bicep) + +### 5. Implement Reusable Validation Functions + +Create shared validation functions for common infrastructure testing patterns. + +**See:** [validation.go](../../../blueprints/full-single-node-cluster/tests/validation.go) and [setup.go](../../../blueprints/full-single-node-cluster/tests/setup.go) + +--- + +## Troubleshooting + +### Contract Test Failures + +**Missing outputs error:** Add outputs to IaC configuration or remove from `BlueprintOutputs` struct to match + +### Tool Installation + +**terraform-docs not found:** Install via package manager (e.g., `brew install terraform-docs`) + +**az bicep not found:** Run `az bicep install` + +### Bicep Output Format + +`GetBicepOutputs` automatically converts Azure CLI's nested output format to flat key-value pairs compatible with Terraform output format. + +--- + +## Complete Working Example + +The [full-single-node-cluster tests](../../../blueprints/full-single-node-cluster/tests/) directory provides a complete, production-ready reference implementation. + +### File Structure + +```text +blueprints/full-single-node-cluster/tests/ +โ”œโ”€โ”€ outputs.go # BlueprintOutputs struct with tf/bicep tags +โ”œโ”€โ”€ contract_terraform_test.go # Terraform contract validation +โ”œโ”€โ”€ contract_bicep_test.go # Bicep contract validation +โ”œโ”€โ”€ deploy_terraform_test.go # Terraform end-to-end deployment test +โ”œโ”€โ”€ deploy_bicep_test.go # Bicep end-to-end deployment test +โ”œโ”€โ”€ validation.go # Reusable validation functions +โ”œโ”€โ”€ setup.go # Post-deployment setup (Arc proxy, RBAC) +โ”œโ”€โ”€ run-contract-tests.sh # Contract test helper script +โ””โ”€โ”€ run-deployment-tests.sh # Deployment test helper script +``` + +**Use these files as templates** when creating tests for your own blueprints. + +--- + +## Additional Resources + +- [Blueprint Test README](../../../blueprints/full-single-node-cluster/tests/README.md) - Complete testing guide +- [Terratest Documentation](https://terratest.gruntwork.io/) - Underlying test framework +- [Azure CLI Bicep Reference](https://learn.microsoft.com/cli/azure/bicep) - Bicep CLI commands diff --git a/src/900-tools-utilities/904-test-utilities/contract.go b/src/900-tools-utilities/904-test-utilities/contract.go new file mode 100644 index 00000000..2743251b --- /dev/null +++ b/src/900-tools-utilities/904-test-utilities/contract.go @@ -0,0 +1,405 @@ +// Package testutil provides reusable testing utilities for blueprint deployment and validation. +// Functions in this file handle output contract validation between IaC and test code. +package testutil + +import ( + "encoding/json" + "os/exec" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// GetTerraformDeclaredOutputs extracts all declared output names from Terraform configuration. +// Uses 'terraform-docs json' to parse outputs.tf without requiring terraform init or plan. +// +// Parameters: +// - t: Test context for assertions and logging +// - terraformDir: Absolute or relative path to directory containing Terraform files +// +// Returns: +// - []string: Output names in the order they appear in outputs.tf +// +// Requirements: +// - terraform-docs must be installed and available on PATH +// - terraformDir must contain valid Terraform configuration files +// +// This is a static analysis tool - no deployment or Azure authentication required. +func GetTerraformDeclaredOutputs(t *testing.T, terraformDir string) []string { + // Run terraform-docs to get output schema + cmd := exec.Command("terraform-docs", "json", terraformDir) + output, err := cmd.CombinedOutput() + require.NoError(t, err, "Failed to run terraform-docs: %s\nOutput: %s", err, string(output)) + + // Parse JSON output - terraform-docs returns outputs as an array of objects with "name" fields + var result struct { + Outputs []struct { + Name string `json:"name"` + } `json:"outputs"` + } + err = json.Unmarshal(output, &result) + require.NoError(t, err, "Failed to parse terraform-docs JSON output: %s", string(output)) + + // Extract output names + keys := make([]string, 0, len(result.Outputs)) + for _, output := range result.Outputs { + keys = append(keys, output.Name) + } + + return keys +} + +// GetBicepDeclaredOutputs extracts all declared output names from Bicep configuration. +// Uses 'az bicep build' to compile Bicep to ARM template JSON, then parses outputs. +// +// Parameters: +// - t: Test context for assertions and logging +// - bicepDir: Absolute or relative path to directory containing main.bicep +// +// Returns: +// - []string: Output names extracted from compiled ARM template +// +// Requirements: +// - Azure CLI with Bicep extension must be installed (az bicep install) +// - bicepDir must contain valid main.bicep file +// +// Note: Output order may differ from declaration order due to ARM template compilation. +// This is a static analysis tool - no deployment or Azure authentication required. +func GetBicepDeclaredOutputs(t *testing.T, bicepDir string) []string { + // Build Bicep to ARM template JSON + // Use separate stdout/stderr to avoid warnings polluting JSON output + bicepFile := filepath.Join(bicepDir, "main.bicep") + cmd := exec.Command("az", "bicep", "build", "--file", bicepFile, "--stdout") + stdout, err := cmd.Output() + require.NoError(t, err, "Failed to compile Bicep: %v", err) + + // Parse ARM template JSON from stdout only + var armTemplate struct { + Outputs map[string]interface{} `json:"outputs"` + } + err = json.Unmarshal(stdout, &armTemplate) + require.NoError(t, err, "Failed to parse ARM template JSON: %s", string(stdout)) + + // Extract output keys + keys := make([]string, 0, len(armTemplate.Outputs)) + for k := range armTemplate.Outputs { + keys = append(keys, k) + } + + return keys +} + +// ValidateTerraformContract is a convenience function for validating Terraform output contracts. +// Combines GetTerraformDeclaredOutputs and ValidateOutputContract into a single call. +// +// Parameters: +// - t: Test context for assertions and logging +// - terraformDir: Absolute or relative path to directory containing Terraform files +// - requiredOutputs: List of output names required by test code (from BlueprintOutputs) +// +// Fails the test if any required outputs are missing from Terraform configuration. +// This is the recommended way for blueprints to implement Terraform contract tests. +func ValidateTerraformContract(t *testing.T, terraformDir string, requiredOutputs []string) { + declaredOutputs := GetTerraformDeclaredOutputs(t, terraformDir) + ValidateOutputContract(t, declaredOutputs, requiredOutputs, "terraform") +} + +// ValidateBicepContract is a convenience function for validating Bicep output contracts. +// Combines GetBicepDeclaredOutputs and ValidateOutputContract into a single call. +// Automatically converts required output names from snake_case to camelCase since +// Bicep uses camelCase for output names while test structs use snake_case tags. +// +// Parameters: +// - t: Test context for assertions and logging +// - bicepDir: Absolute or relative path to directory containing main.bicep +// - requiredOutputs: List of output names required by test code (from BlueprintOutputs) +// These should be in snake_case format and will be converted to camelCase +// +// Fails the test if any required outputs are missing from Bicep configuration. +// This is the recommended way for blueprints to implement Bicep contract tests. +func ValidateBicepContract(t *testing.T, bicepDir string, requiredOutputs []string) { + declaredOutputs := GetBicepDeclaredOutputs(t, bicepDir) + + // Convert required outputs from snake_case to camelCase for Bicep comparison + camelCaseRequiredOutputs := make([]string, len(requiredOutputs)) + for i, output := range requiredOutputs { + camelCaseRequiredOutputs[i] = SnakeToCamelCase(output) + } + + ValidateOutputContract(t, declaredOutputs, camelCaseRequiredOutputs, "bicep") +} + +// ValidateOutputContract verifies all required outputs are declared in IaC configuration. +// Compares declared outputs (from IaC) against required outputs (from test struct). +// +// Parameters: +// - t: Test context for assertions and logging +// - declaredOutputs: Output names extracted from IaC (Terraform or Bicep) +// - requiredOutputs: Output names required by test code +// - framework: Framework name for error messages ("terraform" or "bicep") +// +// Behavior: +// - Logs all declared and required outputs for transparency +// - Identifies missing outputs by comparing slices +// - Reports detailed error with missing output names if validation fails +// - Fails test immediately if any required outputs are missing +// +// This validation ensures compile-time contract between IaC and test code. +func ValidateOutputContract(t *testing.T, declaredOutputs, requiredOutputs []string, framework string) { + t.Logf("Declared outputs in %s: %v", framework, declaredOutputs) + t.Logf("Required outputs by tests: %v", requiredOutputs) + + // Track missing outputs + missing := []string{} + declaredMap := make(map[string]bool) + for _, declared := range declaredOutputs { + declaredMap[declared] = true + } + + for _, required := range requiredOutputs { + if !declaredMap[required] { + missing = append(missing, required) + } + } + + // Report results + if len(missing) > 0 { + t.Errorf("โŒ Missing %d required outputs in %s: %v", len(missing), framework, missing) + if framework == "terraform" { + t.Errorf(" Ensure these outputs are declared in terraform/outputs.tf") + } else if framework == "bicep" { + t.Errorf(" Ensure these outputs are declared in bicep/main.bicep") + } + } else { + t.Logf("โœ… All %d required outputs are declared in %s", len(requiredOutputs), framework) + } + + require.Empty(t, missing, "Output contract validation failed") +} + +// ParseTerraformOutputsFromMap converts raw Terraform deployment outputs to a strongly-typed output struct using reflection. +// Uses struct field tags with `output` key to map output names to struct fields. +// Fails test immediately if any required output (with `output` tag) is missing from raw outputs. +// Note: This validates presence of output keys, not their values. Outputs can be nil/null. +// +// Parameters: +// - t: Test context for assertions and logging +// - raw: Raw output map from Terraform deployment (e.g., terraform.OutputAll) +// - outputStruct: Pointer to struct with `output` tags defining expected outputs +// +// Example usage: +// +// type MyOutputs struct { +// ResourceGroup string `output:"resource_group_name"` +// Location string `output:"location"` +// } +// outputs := &MyOutputs{} +// ParseTerraformOutputsFromMap(t, rawOutputs, outputs) +func ParseTerraformOutputsFromMap(t *testing.T, raw map[string]any, outputStruct any) { + parseOutputsFromMap(t, raw, outputStruct, "terraform", func(key string) string { return key }, false) +} + +// ParseBicepOutputsFromMap converts raw Bicep deployment outputs to a strongly-typed output struct. +// Similar to ParseTerraformOutputsFromMap but converts snake_case output tags to camelCase for Bicep lookup. +// Bicep outputs use camelCase while struct tags use snake_case for consistency with Terraform. +// Also recursively transforms map keys from camelCase to snake_case so validation code works unchanged. +// +// Parameters: +// - t: Test context for assertions and logging +// - raw: Raw output map from Bicep deployment +// - outputStruct: Pointer to struct with `output` tags defining expected outputs (in snake_case) +// +// Example usage: +// +// type MyOutputs struct { +// ResourceGroup string `output:"resource_group_name"` // Maps to "resourceGroupName" in Bicep +// Location string `output:"location"` +// } +// outputs := &MyOutputs{} +// ParseBicepOutputsFromMap(t, rawOutputs, outputs) +func ParseBicepOutputsFromMap(t *testing.T, raw map[string]any, outputStruct any) { + parseOutputsFromMap(t, raw, outputStruct, "bicep", SnakeToCamelCase, true) +} + +// parseOutputsFromMap is the internal implementation for parsing deployment outputs. +// It uses a keyTransform function to convert output tag names before lookup. +// If transformMapKeys is true, it also recursively transforms map keys from camelCase to snake_case. +func parseOutputsFromMap(t *testing.T, raw map[string]any, outputStruct any, framework string, keyTransform func(string) string, transformMapKeys bool) { + v := reflect.ValueOf(outputStruct) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + require.Fail(t, "outputStruct must be a pointer to a struct") + return + } + + v = v.Elem() + typ := v.Type() + + var missingOutputs []string + for i := 0; i < v.NumField(); i++ { + field := typ.Field(i) + outputKey := field.Tag.Get("output") + + if outputKey == "" { + continue + } + + lookupKey := keyTransform(outputKey) + value, exists := raw[lookupKey] + if !exists { + missingOutputs = append(missingOutputs, lookupKey) + continue + } + + // Transform map keys from camelCase to snake_case for Bicep outputs + if transformMapKeys { + value = transformMapKeysRecursive(value) + } + + fieldValue := v.Field(i) + valueType := reflect.TypeOf(value) + if value == nil || !fieldValue.CanSet() || !valueType.AssignableTo(fieldValue.Type()) { + require.Fail(t, "Type mismatch for output", + "Output key '%s' has a type mismatch: expected %s but got %v", + lookupKey, fieldValue.Type(), valueType) + } + fieldValue.Set(reflect.ValueOf(value)) + } + + if len(missingOutputs) > 0 { + var fileHint string + if framework == "terraform" { + fileHint = "terraform/outputs.tf" + } else { + fileHint = "bicep/main.bicep" + } + require.Fail(t, "Missing required deployment outputs", + "The following %d output(s) are missing from deployment outputs: %v\n"+ + "Ensure these outputs are declared in %s", + len(missingOutputs), missingOutputs, fileHint) + } +} + +// transformMapKeysRecursive recursively transforms map keys from camelCase to snake_case. +// This allows Bicep outputs (camelCase keys) to work with validation code that expects snake_case keys. +func transformMapKeysRecursive(value any) any { + if value == nil { + return nil + } + + switch v := value.(type) { + case map[string]any: + result := make(map[string]any, len(v)) + for key, val := range v { + snakeKey := CamelToSnakeCase(key) + result[snakeKey] = transformMapKeysRecursive(val) + } + return result + case []any: + result := make([]any, len(v)) + for i, item := range v { + result[i] = transformMapKeysRecursive(item) + } + return result + default: + return value + } +} + +// GetOutputKeysFromStruct extracts all output names defined in a struct using reflection. +// Uses struct field tags with `output` key to extract output names. +// Returns output names in the order they appear in the struct. +// Used by contract tests to validate IaC output declarations. +// +// Parameters: +// - outputStruct: Struct instance (value or pointer) with `output` tags +// +// Returns: Slice of output names extracted from struct tags +// +// Example usage: +// +// type MyOutputs struct { +// ResourceGroup string `output:"resource_group_name"` +// Location string `output:"location"` +// } +// keys := GetOutputKeysFromStruct(MyOutputs{}) +// // Returns: []string{"resource_group_name", "location"} +func GetOutputKeysFromStruct(outputStruct any) []string { + v := reflect.ValueOf(outputStruct) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + typ := v.Type() + + keys := make([]string, 0, typ.NumField()) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + key := field.Tag.Get("output") + if key != "" { + keys = append(keys, key) + } + } + return keys +} + +// SnakeToCamelCase converts a snake_case string to camelCase. +// Used to transform output names from Terraform convention (snake_case) to Bicep convention (camelCase). +// +// Examples: +// +// "deployment_summary" -> "deploymentSummary" +// "azure_iot_operations" -> "azureIotOperations" +// "acr_network_posture" -> "acrNetworkPosture" +// "nat_gateway_public_ips" -> "natGatewayPublicIps" +func SnakeToCamelCase(s string) string { + if s == "" { + return s + } + + parts := strings.Split(s, "_") + if len(parts) == 1 { + return strings.ToLower(s) + } + + var result strings.Builder + + for i, part := range parts { + if part == "" { + continue + } + if i == 0 { + result.WriteString(strings.ToLower(part)) + } else { + result.WriteString(strings.ToUpper(part[:1]) + strings.ToLower(part[1:])) + } + } + + return result.String() +} + +// CamelToSnakeCase converts a camelCase string to snake_case. +// Used to transform map keys from Bicep convention (camelCase) to Terraform convention (snake_case). +// +// Examples: +// +// "resourceGroup" -> "resource_group" +// "keyVaultName" -> "key_vault_name" +// "aioIdentity" -> "aio_identity" +func CamelToSnakeCase(s string) string { + if s == "" { + return s + } + + var result strings.Builder + + for i, r := range s { + if i > 0 && r >= 'A' && r <= 'Z' { + result.WriteByte('_') + } + result.WriteRune(r) + } + + return strings.ToLower(result.String()) +} diff --git a/src/900-tools-utilities/904-test-utilities/deployment.go b/src/900-tools-utilities/904-test-utilities/deployment.go new file mode 100644 index 00000000..6e0f9eb7 --- /dev/null +++ b/src/900-tools-utilities/904-test-utilities/deployment.go @@ -0,0 +1,271 @@ +// Package testutil provides reusable testing utilities for blueprint deployment and validation. +// Functions in this file handle deployment and output retrieval for both Terraform and Bicep. +package testutil + +import ( + "encoding/json" + "os" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/retry" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/require" +) + +// DeployTerraform executes 'terraform init' and 'terraform apply' with the provided variables. +// +// Parameters: +// - t: Test context for assertions and logging +// - terraformDir: Absolute or relative path to directory containing Terraform files +// - vars: Terraform input variables as key-value map +// - shouldCleanup: If true, automatically destroys resources via t.Cleanup after test +// +// Returns: +// - map[string]any: All deployment outputs where keys are output names +// +// Behavior: +// - Configures retry logic for common transient errors +// - Disables colored output for cleaner log readability +// - Registers cleanup function if shouldCleanup is true +// - Runs terraform init and apply in sequence +// - Retrieves all outputs after successful deployment +// +// Requirements: +// - Terraform must be installed and available on PATH +// - Valid Terraform configuration in terraformDir +// - Azure authentication configured (ARM_SUBSCRIPTION_ID, etc.) +func DeployTerraform(t *testing.T, terraformDir string, vars map[string]any, shouldCleanup bool) map[string]any { + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: terraformDir, + Vars: vars, + NoColor: true, + Lock: true, + }) + + // Defer cleanup of deployed resources only if shouldCleanup is true + if shouldCleanup { + t.Cleanup(func() { + DestroyTerraform(t, terraformOptions) + }) + } else { + t.Log("Skipping resource cleanup: shouldCleanup is false") + } + + // Run terraform init and apply + terraform.InitAndApply(t, terraformOptions) + + // Retrieve and return outputs + outputOptions := &terraform.Options{ + TerraformDir: terraformDir, + Vars: vars, + NoColor: true, + Logger: logger.Discard, + } + return terraform.OutputAll(t, outputOptions) +} + +// DeployBicep executes 'az deployment sub create' for subscription-level Bicep deployment. +// +// Parameters: +// - t: Test context for assertions and logging +// - bicepDir: Absolute or relative path to directory containing main.bicep +// - subscriptionID: Azure subscription ID for deployment +// - deploymentName: Name for the Azure deployment (must be unique per subscription) +// - resourceGroupName: Name of resource group to delete during cleanup +// - params: Bicep input parameters as key-value map +// - shouldCleanup: If true, deletes resource group via t.Cleanup after test +// +// Returns: +// - map[string]any: All deployment outputs converted to Terraform-compatible format +// +// Behavior: +// - Creates temporary parameters JSON file in Azure format +// - Converts params to {"$schema": ..., "parameters": {"key": {"value": val}}} format +// - Executes az deployment sub create at subscription scope +// - Registers resource group deletion if shouldCleanup is true +// - Cleans up temporary parameters file automatically +// - Retrieves outputs and converts from Azure to Terraform format +// +// Requirements: +// - Azure CLI must be installed with Bicep extension +// - User must be authenticated (az login) +// - Sufficient subscription permissions for deployment +// +// Note: Cleanup deletes the entire resource group, not just the deployment. +func DeployBicep(t *testing.T, bicepDir, subscriptionID, deploymentName string, resourceGroupName string, params map[string]any, shouldCleanup bool) map[string]any { + // Create temporary parameters JSON file + tmpFile, err := os.CreateTemp("", "bicep-params-*.json") + require.NoError(t, err, "Failed to create temporary parameters file") + paramsFile := tmpFile.Name() + tmpFile.Close() + + // Ensure cleanup of temporary file + defer os.Remove(paramsFile) + + // Convert parameters to Azure parameter format + azureParams := map[string]any{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": make(map[string]any), + } + + paramMap := azureParams["parameters"].(map[string]any) + for key, value := range params { + paramMap[key] = map[string]any{"value": value} + } + + paramsJSON, err := json.MarshalIndent(azureParams, "", " ") + require.NoError(t, err, "Failed to marshal parameters") + + err = os.WriteFile(paramsFile, paramsJSON, 0644) + require.NoError(t, err, "Failed to write parameters file") + + // Deploy Bicep template + bicepFile := bicepDir + "/main.bicep" + cmd := shell.Command{ + Command: "az", + Args: []string{"deployment", "sub", "create", + "--no-prompt", + "--verbose", + "--name", deploymentName, + "--template-file", bicepFile, + "--parameters", paramsFile, + "--subscription", subscriptionID}, + } + + // Defer cleanup of deployed resources only if shouldCleanup is true + if shouldCleanup { + t.Cleanup(func() { + DestroyBicep(t, subscriptionID, resourceGroupName) + }) + } else { + t.Log("Skipping resource cleanup: shouldCleanup is false") + } + + retry.DoWithRetry(t, "Bicep deployment", 3, 5*time.Second, func() (string, error) { + return shell.RunCommandAndGetOutputE(t, cmd) + }) + + // Retrieve and return outputs + return GetBicepOutputs(t, deploymentName) +} + +// GetBicepOutputs retrieves outputs from an existing Bicep deployment without redeploying. +// +// Parameters: +// - t: Test context for assertions and logging +// - deploymentName: Name of existing Azure deployment to query +// +// Returns: +// - map[string]any: Deployment outputs in Terraform-compatible format +// +// Behavior: +// - Executes 'az deployment sub show' to query deployment outputs +// - Retries on transient failures (3 attempts, 5 second delay) +// - Converts Azure output format to Terraform-compatible format +// +// Output Format Conversion: +// +// Azure CLI returns: {"outputName": {"type": "string", "value": "actual_value"}} +// This function returns: {"outputName": "actual_value"} +// +// Requirements: +// - Azure CLI must be installed and authenticated +// - Deployment must exist in the current subscription +// +// Use Case: +// +// Fetch outputs from existing deployment when SKIP_BICEP_DEPLOYMENT=true +func GetBicepOutputs(t *testing.T, deploymentName string) map[string]any { + // Get deployment outputs using Azure CLI + cmd := shell.Command{ + Command: "az", + Args: []string{"deployment", "sub", "show", + "--name", deploymentName, + "--query", "properties.outputs", + "-o", "json"}, + } + + output := retry.DoWithRetry(t, "Get Bicep outputs", 3, 5*time.Second, func() (string, error) { + return shell.RunCommandAndGetOutputE(t, cmd) + }) + + outputBytes := []byte(output) + + var azureOutputs map[string]map[string]any + err := json.Unmarshal(outputBytes, &azureOutputs) + require.NoError(t, err, "Failed to parse Bicep outputs") + + // Convert Azure CLI output format to Terraform-compatible format + // Azure outputs are in format: {"outputName": {"type": "string", "value": "actual_value"}} + // Terraform outputs are in format: {"outputName": "actual_value"} + terraformCompatible := make(map[string]any) + for key, outputObj := range azureOutputs { + if val, ok := outputObj["value"]; ok { + terraformCompatible[key] = val + } + } + + return terraformCompatible +} + +// DestroyTerraform executes 'terraform destroy' to tear down all deployed resources. +// +// Parameters: +// - t: Test context for logging +// - terraformOptions: Terraform options used during deployment (must match original) +// +// Behavior: +// - Uses terraformOptions to ensure proper resource identification +// - Terratest's built-in retry logic handles transient errors +// - Blocks until all resources are destroyed +// +// Requirements: +// - Terraform must be installed and available on PATH +// - Azure authentication must be configured +// - terraformOptions must match those used for deployment +// +// Note: Typically called automatically by DeployTerraform when shouldCleanup=true. +func DestroyTerraform(t *testing.T, terraformOptions *terraform.Options) { + t.Log("Destroying Terraform-managed resources...") + + terraform.Destroy(t, terraformOptions) +} + +// DestroyBicep executes 'az group delete' to tear down the resource group and all resources. +// +// Parameters: +// - t: Test context for logging +// - subscriptionID: Azure subscription ID containing the resource group +// - resourceGroupName: Name of resource group to delete +// +// Behavior: +// - Deletes resource group without confirmation prompt (--yes) +// - Returns immediately without waiting for completion (--no-wait) +// - Retries on transient failures (3 attempts, 5 second delay) +// +// Requirements: +// - Azure CLI must be installed and authenticated +// - Sufficient permissions to delete resource groups +// +// Warning: Deletes ALL resources in the resource group, not just those from deployment. +// Note: Typically called automatically by DeployBicep when shouldCleanup=true. +func DestroyBicep(t *testing.T, subscriptionID, resourceGroupName string) { + t.Log("Destroying Bicep-managed resources...") + + cmd := shell.Command{ + Command: "az", + Args: []string{"group", "delete", + "--name", resourceGroupName, + "--subscription", subscriptionID, + "--yes", + "--no-wait"}, + } + + retry.DoWithRetry(t, "Destroy Bicep resources", 3, 5*time.Second, func() (string, error) { + return shell.RunCommandAndGetOutputE(t, cmd) + }) +} diff --git a/src/900-tools-utilities/904-test-utilities/go.mod b/src/900-tools-utilities/904-test-utilities/go.mod new file mode 100644 index 00000000..c18c5c29 --- /dev/null +++ b/src/900-tools-utilities/904-test-utilities/go.mod @@ -0,0 +1,41 @@ +module github.com/microsoft/edge-ai/src/900-tools-utilities/904-test-utilities + +go 1.25.5 + +require ( + github.com/gruntwork-io/terratest v0.54.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-getter/v2 v2.2.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.22.0 // indirect + github.com/hashicorp/terraform-json v0.23.0 // indirect + github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a // indirect + github.com/klauspost/compress v1.16.5 // indirect + github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tmccombs/hcl2json v0.6.4 // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + github.com/zclconf/go-cty v1.15.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/src/900-tools-utilities/904-test-utilities/go.sum b/src/900-tools-utilities/904-test-utilities/go.sum new file mode 100644 index 00000000..94e2e56f --- /dev/null +++ b/src/900-tools-utilities/904-test-utilities/go.sum @@ -0,0 +1,74 @@ +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/gruntwork-io/terratest v0.54.0 h1:JOVATYDpU0NAPbEkgYUP50BR2m45UGiR4dbs20sKzck= +github.com/gruntwork-io/terratest v0.54.0/go.mod h1:QvwQWZMTJmJB4E0d1Uc18quQm7+X53liKKp+fJSuaKA= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk= +github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= +github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= +github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= +github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 h1:ofNAzWCcyTALn2Zv40+8XitdzCgXY6e9qvXwN9W0YXg= +github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVLGw= +github.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/900-tools-utilities/README.md b/src/900-tools-utilities/README.md index 719705b9..35989c7f 100644 --- a/src/900-tools-utilities/README.md +++ b/src/900-tools-utilities/README.md @@ -21,6 +21,7 @@ The 900-tools-utilities components (900-999 range) provide essential development | **[901-video-tools](./901-video-tools/README.md)** | Video processing utilities including video-to-GIF conversion for captured media files | | **[902-aio-sidekick](./902-aio-sidekick/README.md)** | Azure IoT Operations diagnostic and troubleshooting sidekick utility | | **[903-multi-asset-deploy](./903-multi-asset-deploy/README.md)** | Batch deployment tool for multiple IoT assets from CSV files | +| **[904-test-utilities](./904-test-utilities/README.md)** | Shared testing utilities for blueprint validation across Terraform and Bicep | ## Framework Support