1 Commits

Author SHA1 Message Date
Henrik Jess Nielsen
0025043999 backup: uncommitted changes from MAC-M9FQ0900T3 2026-05-17 15:52:31 2026-05-17 15:52:31 +02:00
12 changed files with 450 additions and 992 deletions

View File

@@ -12,6 +12,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends curl unzip ca-c
COPY scripts/download_bicep_ls.sh /scripts/
RUN chmod +x /scripts/download_bicep_ls.sh && BICEP_VERSION=${BICEP_VERSION} /scripts/download_bicep_ls.sh
# Download Azure DevOps pipeline schema for YAML task completions
RUN curl -f -o /azdo-pipeline-schema.json \
"https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/main/service-schema.json"
# ── Stage 2: Python wheel build ───────────────────────────────────────────────
FROM python:3.12-slim AS builder
@@ -41,6 +45,7 @@ RUN apt-get update \
# Copy Bicep Language Server (baked in at build time — no volume needed)
COPY --from=bicep-downloader /opt/bicep-langserver /opt/bicep-langserver
COPY --from=bicep-downloader /azdo-pipeline-schema.json /azdo-pipeline-schema.json
# Install Python package and dependencies
COPY --from=builder /dist/*.whl /tmp/

View File

@@ -14,49 +14,17 @@ supports LSP WebSocket transport.
## IntelliJ IDEA setup (LSP4IJ)
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then add language servers:
### Bicep
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then:
1. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP Bicep`
- Server type: `Command` (not WebSocket — see note below)
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/bicep`
- Arguments: (leave blank)
- File pattern: `*.bicep;*.bicepparam`
- Server type: `WebSocket`
- URL: `wss://ilsp.i80.dk/bicep`
### YAML (Azure DevOps + GitHub Actions)
2. **Add a file-type mapping** under the new server entry:
- File type: `Bicep` (or pattern `*.bicep`)
2. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP YAML`
- Server type: `Command`
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/yaml`
- Arguments: (leave blank)
- File pattern: `*.yaml;*.yml;azure-pipelines.yml`
### Python (optional)
3. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP Python`
- Server type: `Command`
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/python`
- Arguments: (leave blank)
- File pattern: `*.py`
> **Note**: LSP4IJ's WebSocket mode doesn't handle LSP Content-Length framing correctly,
> causing "starting..." to hang or "Stream closed" errors. Use `lsp_bridge_debug.sh` wrapper
> which calls `lsp_bridge.py` and logs errors to `/tmp/lsp_bridge_debug.log` for debugging.
### Troubleshooting
If the language server doesn't start, check the debug log:
```bash
tail -f /tmp/lsp_bridge_debug.log
```
### Disable conflicting plugins
**Disable Azure Toolkit Bicep completions** — this is critical if you have the
3. **Disable Azure Toolkit Bicep completions** — this is critical if you have the
Azure Toolkit plugin installed. Without this step, IDEA merges completions from
both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`,
`projectName`, Bicep schema types, etc.) appearing alongside iLSP results.

View File

@@ -1,684 +0,0 @@
[
"ACCESS_REVIEW_OPERATOR_SERVICE_ROLE",
"ACRDELETE",
"ACRIMAGESIGNER",
"ACRPULL",
"ACRPUSH",
"ACRQUARANTINEREADER",
"ACRQUARANTINEWRITER",
"ADVISOR_RECOMMENDATIONS_CONTRIBUTOR_ASSESSMENTS_AND_REVIEWS",
"ADVISOR_REVIEWS_CONTRIBUTOR",
"ADVISOR_REVIEWS_READER",
"AGENTLESS_SCANNING_FOR_SERVERLESS_SCANNER_SERVICE_ROLE",
"AGFOOD_PLATFORM_DATASET_ADMIN",
"AGFOOD_PLATFORM_SENSOR_PARTNER_CONTRIBUTOR",
"AGFOOD_PLATFORM_SERVICE_ADMIN",
"AGFOOD_PLATFORM_SERVICE_CONTRIBUTOR",
"AGFOOD_PLATFORM_SERVICE_READER",
"ANYBUILD_BUILDER",
"API_MANAGEMENT_DEVELOPER_PORTAL_CONTENT_EDITOR",
"API_MANAGEMENT_SERVICE_CONTRIBUTOR",
"API_MANAGEMENT_SERVICE_OPERATOR_ROLE",
"API_MANAGEMENT_SERVICE_READER_ROLE",
"API_MANAGEMENT_SERVICE_WORKSPACE_API_DEVELOPER",
"API_MANAGEMENT_SERVICE_WORKSPACE_API_PRODUCT_MANAGER",
"API_MANAGEMENT_WORKSPACE_API_DEVELOPER",
"API_MANAGEMENT_WORKSPACE_API_PRODUCT_MANAGER",
"API_MANAGEMENT_WORKSPACE_CONTRIBUTOR",
"API_MANAGEMENT_WORKSPACE_READER",
"APPGW_FOR_CONTAINERS_CONFIGURATION_MANAGER",
"APPLICATION_GROUP_CONTRIBUTOR",
"APPLICATION_INSIGHTS_COMPONENT_CONTRIBUTOR",
"APPLICATION_INSIGHTS_SNAPSHOT_DEBUGGER",
"APP_COMPLIANCE_AUTOMATION_ADMINISTRATOR",
"APP_COMPLIANCE_AUTOMATION_READER",
"APP_CONFIGURATION_CONTRIBUTOR",
"APP_CONFIGURATION_DATA_OWNER",
"APP_CONFIGURATION_DATA_READER",
"APP_CONFIGURATION_DATA_SAS_USER",
"APP_CONFIGURATION_READER",
"APP_SERVICE_ENVIRONMENT_CONTRIBUTOR",
"ARC_GATEWAY_MANAGER",
"ATTESTATION_CONTRIBUTOR",
"ATTESTATION_READER",
"AUTOMATION_CONTRIBUTOR",
"AUTOMATION_JOB_OPERATOR",
"AUTOMATION_OPERATOR",
"AUTOMATION_RUNBOOK_OPERATOR",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_CONTRIBUTOR_PREVIEW",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_OWNER_PREVIEW",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_READER_PREVIEW",
"AUTO_ACTIONS_CONTRIBUTOR",
"AVERE_CONTRIBUTOR",
"AVERE_OPERATOR",
"AVS_ON_FLEET_VIS_ROLE",
"AVS_ORCHESTRATOR_ROLE",
"AZUREML_COMPUTE_OPERATOR",
"AZUREML_DATA_SCIENTIST",
"AZUREML_METRICS_WRITER_PREVIEW",
"AZUREML_REGISTRY_USER",
"AZURE_AI_ACCOUNT_OWNER",
"AZURE_AI_ADMINISTRATOR",
"AZURE_AI_DEVELOPER",
"AZURE_AI_ENTERPRISE_NETWORK_CONNECTION_APPROVER",
"AZURE_AI_INFERENCE_DEPLOYMENT_OPERATOR",
"AZURE_AI_PROJECT_MANAGER",
"AZURE_AI_SAFETY_EVALUATOR",
"AZURE_AI_USER",
"AZURE_API_CENTER_COMPLIANCE_MANAGER",
"AZURE_API_CENTER_CREDENTIAL_ACCESS_READER",
"AZURE_API_CENTER_DATA_READER",
"AZURE_API_CENTER_SERVICE_CONTRIBUTOR",
"AZURE_API_CENTER_SERVICE_READER",
"AZURE_ARC_ENABLED_KUBERNETES_CLUSTER_USER_ROLE",
"AZURE_ARC_KUBERNETES_ADMIN",
"AZURE_ARC_KUBERNETES_CLUSTER_ADMIN",
"AZURE_ARC_KUBERNETES_VIEWER",
"AZURE_ARC_KUBERNETES_WRITER",
"AZURE_ARC_SCVMM_ADMINISTRATOR_ROLE",
"AZURE_ARC_SCVMM_PRIVATE_CLOUDS_ONBOARDING",
"AZURE_ARC_SCVMM_PRIVATE_CLOUD_USER",
"AZURE_ARC_SCVMM_VM_CONTRIBUTOR",
"AZURE_ARC_VMWARE_ADMINISTRATOR_ROLE",
"AZURE_ARC_VMWARE_PRIVATE_CLOUDS_ONBOARDING",
"AZURE_ARC_VMWARE_PRIVATE_CLOUD_USER",
"AZURE_ARC_VMWARE_VM_CONTRIBUTOR",
"AZURE_AUTOMANAGE_CONTRIBUTOR",
"AZURE_BACKUP_SNAPSHOT_CONTRIBUTOR",
"AZURE_BATCH_ACCOUNT_CONTRIBUTOR",
"AZURE_BATCH_ACCOUNT_READER",
"AZURE_BATCH_DATA_CONTRIBUTOR",
"AZURE_BATCH_JOB_SUBMITTER",
"AZURE_BATCH_SERVICE_ORCHESTRATION_ROLE",
"AZURE_BOT_SERVICE_CONTRIBUTOR_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_ADMINISTRATOR",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_MANAGEMENT_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_READER",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_SERVICE_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_SERVICE_ROLE_FOR_MANAGEMENT",
"AZURE_CONNECTED_MACHINE_ONBOARDING",
"AZURE_CONNECTED_MACHINE_RESOURCE_ADMINISTRATOR",
"AZURE_CONNECTED_MACHINE_RESOURCE_MANAGER",
"AZURE_CONNECTED_SQL_SERVER_ONBOARDING",
"AZURE_CONTAINERAPPS_SESSION_EXECUTOR",
"AZURE_CONTAINER_INSTANCES_CONTRIBUTOR_ROLE",
"AZURE_CONTAINER_REGISTRY_SECURE_SUPPLY_CHAIN_OPERATOR_SERVICE_ROLE",
"AZURE_CONTAINER_STORAGE_CONTRIBUTOR",
"AZURE_CONTAINER_STORAGE_OPERATOR",
"AZURE_CONTAINER_STORAGE_OWNER",
"AZURE_CUSTOMER_LOCKBOX_APPROVER_FOR_SUBSCRIPTION",
"AZURE_DEPLOYMENT_STACK_CONTRIBUTOR",
"AZURE_DEPLOYMENT_STACK_OWNER",
"AZURE_DEVICE_ONBOARDING_DISCOVERY_CONTRIBUTOR",
"AZURE_DEVICE_UPDATE_AGENT",
"AZURE_DIGITAL_TWINS_DATA_OWNER",
"AZURE_DIGITAL_TWINS_DATA_READER",
"AZURE_EDGE_HARDWARE_CENTER_ADMINISTRATOR",
"AZURE_EDGE_ON_SITE_DEPLOYMENT_ENGINEER",
"AZURE_EVENT_HUBS_DATA_OWNER",
"AZURE_EVENT_HUBS_DATA_RECEIVER",
"AZURE_EVENT_HUBS_DATA_SENDER",
"AZURE_EXTENSION_FOR_SQL_SERVER_DEPLOYMENT",
"AZURE_FILE_SYNC_ADMINISTRATOR",
"AZURE_FILE_SYNC_READER",
"AZURE_FRONT_DOOR_DOMAIN_CONTRIBUTOR",
"AZURE_FRONT_DOOR_DOMAIN_READER",
"AZURE_FRONT_DOOR_PROFILE_READER",
"AZURE_FRONT_DOOR_SECRET_CONTRIBUTOR",
"AZURE_FRONT_DOOR_SECRET_READER",
"AZURE_HYBRID_DATABASE_ADMINISTRATOR_READ_ONLY_SERVICE_ROLE",
"AZURE_IMPACT_INSIGHT_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_FLEET_MANAGER_HUB_AGENT_ROLE",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_ADMIN",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_ADMIN",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_WRITER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_WRITER",
"AZURE_KUBERNETES_SERVICE_AGENT_POOL_MANAGER_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_CLUSTER_MONITORING_USER",
"AZURE_KUBERNETES_SERVICE_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_POLICY_ADD_ON_DEPLOYMENT",
"AZURE_KUBERNETES_SERVICE_RBAC_ADMIN",
"AZURE_KUBERNETES_SERVICE_RBAC_CLUSTER_ADMIN",
"AZURE_KUBERNETES_SERVICE_RBAC_READER",
"AZURE_KUBERNETES_SERVICE_RBAC_WRITER",
"AZURE_MACHINE_LEARNING_WORKSPACE_CONNECTION_SECRETS_READER",
"AZURE_MANAGED_GRAFANA_WORKSPACE_CONTRIBUTOR",
"AZURE_MAPS_CONTRIBUTOR",
"AZURE_MAPS_DATA_CONTRIBUTOR",
"AZURE_MAPS_DATA_READER",
"AZURE_MAPS_DATA_READ_AND_BATCH_ROLE",
"AZURE_MAPS_SEARCH_AND_RENDER_DATA_READER",
"AZURE_MESSAGING_CATALOG_DATA_OWNER",
"AZURE_MESSAGING_CONNECTORS_OWNER",
"AZURE_MONITOR_DASHBOARDS_WITH_GRAFANA_CONTRIBUTOR",
"AZURE_PROGRAMMABLE_CONNECTIVITY_GATEWAY_DATAPLANE_USER",
"AZURE_PROGRAMMABLE_CONNECTIVITY_GATEWAY_USER",
"AZURE_RED_HAT_OPENSHIFT_CLOUD_CONTROLLER_MANAGER",
"AZURE_RED_HAT_OPENSHIFT_CLUSTER_INGRESS_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_DISK_STORAGE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_FEDERATED_CREDENTIAL",
"AZURE_RED_HAT_OPENSHIFT_FILE_STORAGE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_CLUSTER_API_PROVIDER",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_CONTROL_PLANE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_SERVICE_MANAGED_IDENTITY",
"AZURE_RED_HAT_OPENSHIFT_IMAGE_REGISTRY_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_MACHINE_API_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_NETWORK_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_SERVICE_OPERATOR",
"AZURE_RELAY_LISTENER",
"AZURE_RELAY_OWNER",
"AZURE_RELAY_SENDER",
"AZURE_RESOURCE_BRIDGE_DEPLOYMENT_ROLE",
"AZURE_RESOURCE_NOTIFICATIONS_SYSTEM_TOPICS_SUBSCRIBER",
"AZURE_SERVICE_BUS_DATA_OWNER",
"AZURE_SERVICE_BUS_DATA_RECEIVER",
"AZURE_SERVICE_BUS_DATA_SENDER",
"AZURE_SPHERE_CONTRIBUTOR",
"AZURE_SPHERE_OWNER",
"AZURE_SPHERE_PUBLISHER",
"AZURE_SPHERE_READER",
"AZURE_SPRING_APPS_APPLICATION_CONFIGURATION_SERVICE_CONFIG_FILE_PATTERN_READER_ROLE",
"AZURE_SPRING_APPS_APPLICATION_CONFIGURATION_SERVICE_LOG_READER_ROLE",
"AZURE_SPRING_APPS_CONNECT_ROLE",
"AZURE_SPRING_APPS_JOB_EXECUTION_INSTANCE_LIST_ROLE",
"AZURE_SPRING_APPS_JOB_LOG_READER_ROLE",
"AZURE_SPRING_APPS_MANAGED_COMPONENTS_LOG_READER_ROLE",
"AZURE_SPRING_APPS_REMOTE_DEBUGGING_ROLE",
"AZURE_SPRING_APPS_SPRING_CLOUD_CONFIG_SERVER_LOG_READER_ROLE",
"AZURE_SPRING_APPS_SPRING_CLOUD_GATEWAY_LOG_READER_ROLE",
"AZURE_SPRING_CLOUD_CONFIG_SERVER_CONTRIBUTOR",
"AZURE_SPRING_CLOUD_CONFIG_SERVER_READER",
"AZURE_SPRING_CLOUD_DATA_READER",
"AZURE_SPRING_CLOUD_SERVICE_REGISTRY_CONTRIBUTOR",
"AZURE_SPRING_CLOUD_SERVICE_REGISTRY_READER",
"AZURE_STACK_HCI_ADMINISTRATOR",
"AZURE_STACK_HCI_CONNECTED_INFRAVMS",
"AZURE_STACK_HCI_DEVICE_MANAGEMENT_ROLE",
"AZURE_STACK_HCI_EDGE_MACHINE_CONTRIBUTOR_ROLE",
"AZURE_STACK_HCI_VM_CONTRIBUTOR",
"AZURE_STACK_HCI_VM_READER",
"AZURE_STACK_REGISTRATION_OWNER",
"AZURE_USAGE_BILLING_DATA_SENDER",
"AZURE_VM_MANAGED_IDENTITIES_RESTORE_CONTRIBUTOR",
"BACKUP_CONTRIBUTOR",
"BACKUP_MUA_ADMIN",
"BACKUP_MUA_OPERATOR",
"BACKUP_OPERATOR",
"BACKUP_READER",
"BAYER_AG_POWERED_SERVICES_CROP_ID_SOLUTION_USER_ROLE",
"BAYER_AG_POWERED_SERVICES_CWUM_SOLUTION",
"BAYER_AG_POWERED_SERVICES_FIELD_IMAGERY_SOLUTION_SERVICE_ROLE",
"BAYER_AG_POWERED_SERVICES_GDU_SOLUTION",
"BAYER_AG_POWERED_SERVICES_HISTORICAL_WEATHER_DATA_SOLUTION_USER_ROLE",
"BAYER_AG_POWERED_SERVICES_IMAGERY_SOLUTION",
"BAYER_AG_POWERED_SERVICES_SMART_BOUNDARY_SOLUTION_USER_ROLE",
"BILLING_READER",
"BIZTALK_CONTRIBUTOR",
"BLOCKCHAIN_MEMBER_NODE_ACCESS_PREVIEW",
"BLUEPRINT_CONTRIBUTOR",
"BLUEPRINT_OPERATOR",
"CARBON_OPTIMIZATION_READER",
"CDN_ENDPOINT_CONTRIBUTOR",
"CDN_ENDPOINT_READER",
"CDN_PROFILE_CONTRIBUTOR",
"CDN_PROFILE_READER",
"CHAOS_STUDIO_EXPERIMENT_CONTRIBUTOR",
"CHAOS_STUDIO_OPERATOR",
"CHAOS_STUDIO_READER",
"CHAOS_STUDIO_TARGET_CONTRIBUTOR",
"CLASSIC_NETWORK_CONTRIBUTOR",
"CLASSIC_STORAGE_ACCOUNT_CONTRIBUTOR",
"CLASSIC_STORAGE_ACCOUNT_KEY_OPERATOR_SERVICE_ROLE",
"CLASSIC_VIRTUAL_MACHINE_CONTRIBUTOR",
"CLEARDB_MYSQL_DB_CONTRIBUTOR",
"CLOUDTEST_CONTRIBUTOR_ROLE",
"COGNITIVE_SEARCH_SERVERLESS_DATA_CONTRIBUTOR_DEPRECATED",
"COGNITIVE_SEARCH_SERVERLESS_DATA_READER_DEPRECATED",
"COGNITIVE_SERVICES_CONTRIBUTOR",
"COGNITIVE_SERVICES_CUSTOM_VISION_CONTRIBUTOR",
"COGNITIVE_SERVICES_CUSTOM_VISION_DEPLOYMENT",
"COGNITIVE_SERVICES_CUSTOM_VISION_LABELER",
"COGNITIVE_SERVICES_CUSTOM_VISION_READER",
"COGNITIVE_SERVICES_CUSTOM_VISION_TRAINER",
"COGNITIVE_SERVICES_DATA_CONTRIBUTOR_PREVIEW",
"COGNITIVE_SERVICES_DATA_READER",
"COGNITIVE_SERVICES_FACE_CONTRIBUTOR",
"COGNITIVE_SERVICES_FACE_RECOGNIZER",
"COGNITIVE_SERVICES_IMMERSIVE_READER_USER",
"COGNITIVE_SERVICES_LANGUAGE_OWNER",
"COGNITIVE_SERVICES_LANGUAGE_READER",
"COGNITIVE_SERVICES_LANGUAGE_WRITER",
"COGNITIVE_SERVICES_LUIS_OWNER",
"COGNITIVE_SERVICES_LUIS_READER",
"COGNITIVE_SERVICES_LUIS_WRITER",
"COGNITIVE_SERVICES_METRICS_ADVISOR_ADMINISTRATOR",
"COGNITIVE_SERVICES_METRICS_ADVISOR_USER",
"COGNITIVE_SERVICES_OPENAI_CONTRIBUTOR",
"COGNITIVE_SERVICES_OPENAI_USER",
"COGNITIVE_SERVICES_QNA_MAKER_EDITOR",
"COGNITIVE_SERVICES_QNA_MAKER_READER",
"COGNITIVE_SERVICES_SPEECH_CONTRIBUTOR",
"COGNITIVE_SERVICES_SPEECH_USER",
"COGNITIVE_SERVICES_USAGES_READER",
"COGNITIVE_SERVICES_USER",
"COLLABORATIVE_DATA_CONTRIBUTOR",
"COLLABORATIVE_RUNTIME_OPERATOR",
"COMMUNICATION_AND_EMAIL_SERVICE_OWNER",
"COMMUNITY_CONTRIBUTOR_ROLE",
"COMMUNITY_OWNER_ROLE",
"COMMUNITY_READER_ROLE",
"COMPUTE_DIAGNOSTICS_ROLE",
"COMPUTE_FLEET_CONTRIBUTOR",
"COMPUTE_GALLERY_ARTIFACTS_PUBLISHER",
"COMPUTE_GALLERY_IMAGE_READER",
"COMPUTE_GALLERY_SHARING_ADMIN",
"COMPUTE_RECOMMENDATIONS_ROLE",
"CONNECTED_CLUSTER_MANAGED_IDENTITY_CHECKACCESS_READER",
"CONNECTOR_READER",
"CONTAINERAPP_READER",
"CONTAINER_APPS_CONNECTEDENVIRONMENTS_CONTRIBUTOR",
"CONTAINER_APPS_CONNECTEDENVIRONMENTS_READER",
"CONTAINER_APPS_CONTRIBUTOR",
"CONTAINER_APPS_JOBS_CONTRIBUTOR",
"CONTAINER_APPS_JOBS_OPERATOR",
"CONTAINER_APPS_JOBS_READER",
"CONTAINER_APPS_MANAGEDENVIRONMENTS_CONTRIBUTOR",
"CONTAINER_APPS_MANAGEDENVIRONMENTS_READER",
"CONTAINER_APPS_OPERATOR",
"CONTAINER_APPS_SESSIONPOOLS_CONTRIBUTOR",
"CONTAINER_APPS_SESSIONPOOLS_READER",
"CONTAINER_INSTANCES_CONTRIBUTOR",
"CONTAINER_REGISTRY_CACHE_RULE_ADMINISTRATOR",
"CONTAINER_REGISTRY_CACHE_RULE_READER",
"CONTAINER_REGISTRY_CONFIGURATION_READER_AND_DATA_ACCESS_CONFIGURATION_READER",
"CONTAINER_REGISTRY_CONTRIBUTOR_AND_DATA_ACCESS_CONFIGURATION_ADMINISTRATOR",
"CONTAINER_REGISTRY_CREDENTIAL_SET_ADMINISTRATOR",
"CONTAINER_REGISTRY_CREDENTIAL_SET_READER",
"CONTAINER_REGISTRY_DATA_IMPORTER_AND_DATA_READER",
"CONTAINER_REGISTRY_REPOSITORY_CATALOG_LISTER",
"CONTAINER_REGISTRY_REPOSITORY_CONTRIBUTOR",
"CONTAINER_REGISTRY_REPOSITORY_READER",
"CONTAINER_REGISTRY_REPOSITORY_WRITER",
"CONTAINER_REGISTRY_TASKS_CONTRIBUTOR",
"CONTAINER_REGISTRY_TRANSFER_PIPELINE_CONTRIBUTOR",
"CONTRIBUTOR",
"COSMOSBACKUPOPERATOR",
"COSMOSRESTOREOPERATOR",
"COSMOS_DB_ACCOUNT_READER_ROLE",
"COSMOS_DB_OPERATOR",
"COST_MANAGEMENT_CONTRIBUTOR",
"COST_MANAGEMENT_READER",
"CROSSCONNECTIONMANAGER",
"CROSSCONNECTIONREADER",
"CUSTOM_AUTOMATION_ROLE_ROOT",
"DATA_BOUNDARY_TENANT_ADMINISTRATOR",
"DATA_BOX_CONTRIBUTOR",
"DATA_BOX_READER",
"DATA_FACTORY_CONTRIBUTOR",
"DATA_LABELING_LABELER",
"DATA_LAKE_ANALYTICS_DEVELOPER",
"DATA_OPERATOR_FOR_MANAGED_DISKS",
"DATA_PURGER",
"DEDICATED_HOST_CONTRIBUTOR_ROLE",
"DEFENDER_CSPM_STORAGE_DATA_SCANNER",
"DEFENDER_CSPM_STORAGE_SCANNER_OPERATOR",
"DEFENDER_FOR_STORAGE_DATA_SCANNER",
"DEFENDER_FOR_STORAGE_SCANNER_OPERATOR",
"DEFENDER_KUBERNETES_AGENT_OPERATOR",
"DEID_BATCH_DATA_OWNER",
"DEID_BATCH_DATA_READER",
"DEID_DATA_OWNER",
"DEID_REALTIME_DATA_USER",
"DEPLOYMENT_ENVIRONMENTS_READER",
"DEPLOYMENT_ENVIRONMENTS_USER",
"DESKTOP_VIRTUALIZATION_APPLICATION_GROUP_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_APPLICATION_GROUP_READER",
"DESKTOP_VIRTUALIZATION_APP_ATTACH_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_HOST_POOL_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_HOST_POOL_READER",
"DESKTOP_VIRTUALIZATION_POWER_ON_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_POWER_ON_OFF_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_READER",
"DESKTOP_VIRTUALIZATION_SESSION_HOST_OPERATOR",
"DESKTOP_VIRTUALIZATION_USER",
"DESKTOP_VIRTUALIZATION_USER_SESSION_OPERATOR",
"DESKTOP_VIRTUALIZATION_VIRTUAL_MACHINE_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_WORKSPACE_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_WORKSPACE_READER",
"DEVCENTER_DEV_BOX_USER",
"DEVCENTER_PROJECT_ADMIN",
"DEVICE_PROVISIONING_SERVICE_DATA_CONTRIBUTOR",
"DEVICE_PROVISIONING_SERVICE_DATA_READER",
"DEVICE_UPDATE_ADMINISTRATOR",
"DEVICE_UPDATE_CONTENT_ADMINISTRATOR",
"DEVICE_UPDATE_CONTENT_READER",
"DEVICE_UPDATE_DEPLOYMENTS_ADMINISTRATOR",
"DEVICE_UPDATE_DEPLOYMENTS_READER",
"DEVICE_UPDATE_READER",
"DEVTEST_LABS_USER",
"DICOM_DATA_OWNER",
"DICOM_DATA_READER",
"DISK_BACKUP_READER",
"DISK_ENCRYPTION_SET_OPERATOR_FOR_MANAGED_DISKS",
"DISK_POOL_OPERATOR",
"DISK_RESTORE_OPERATOR",
"DISK_SNAPSHOT_CONTRIBUTOR",
"DNS_RESOLVER_CONTRIBUTOR",
"DNS_ZONE_CONTRIBUTOR",
"DOCUMENTDB_ACCOUNT_CONTRIBUTOR",
"DOMAIN_SERVICES_CONTRIBUTOR",
"DOMAIN_SERVICES_READER",
"DURABLE_TASK_DATA_CONTRIBUTOR",
"DURABLE_TASK_DATA_READER",
"DURABLE_TASK_WORKER",
"EDGE_MANAGEMENT_COPILOT_USER",
"ELASTIC_SAN_NETWORK_ADMIN",
"ELASTIC_SAN_OWNER",
"ELASTIC_SAN_READER",
"ELASTIC_SAN_SNAPSHOT_EXPORTER",
"ELASTIC_SAN_VOLUME_GROUP_OWNER",
"ELASTIC_SAN_VOLUME_IMPORTER",
"ENCLAVE_APPROVER_ROLE",
"ENCLAVE_CONTRIBUTOR_ROLE",
"ENCLAVE_OWNER_ROLE",
"ENCLAVE_READER_ROLE",
"EVENTGRID_CONTRIBUTOR",
"EVENTGRID_DATA_CONTRIBUTOR",
"EVENTGRID_DATA_RECEIVER",
"EVENTGRID_DATA_SENDER",
"EVENTGRID_EVENTSUBSCRIPTION_CONTRIBUTOR",
"EVENTGRID_EVENTSUBSCRIPTION_READER",
"EVENTGRID_TOPICSPACES_PUBLISHER",
"EVENTGRID_TOPICSPACES_SUBSCRIBER",
"EXPERIMENTATION_ADMINISTRATOR",
"EXPERIMENTATION_CONTRIBUTOR",
"EXPERIMENTATION_METRIC_CONTRIBUTOR",
"EXPERIMENTATION_READER",
"FHIR_DATA_CONTRIBUTOR",
"FHIR_DATA_CONVERTER",
"FHIR_DATA_EXPORTER",
"FHIR_DATA_IMPORTER",
"FHIR_DATA_READER",
"FHIR_DATA_WRITER",
"FHIR_SMART_USER",
"FIRMWARE_ANALYSIS_ADMIN",
"FLUX_CONFIGURATIONS_CONTRIBUTOR",
"GEOCATALOG_ADMINISTRATOR",
"GEOCATALOG_READER",
"GITOPS_LZ_LIST_ACTIONS",
"GRAFANA_ADMIN",
"GRAFANA_EDITOR",
"GRAFANA_LIMITED_VIEWER",
"GRAFANA_VIEWER",
"GRAPH_OWNER",
"GROUPQUOTA_READER",
"GROUPQUOTA_REQUEST_OPERATOR",
"GUEST_CONFIGURATION_RESOURCE_CONTRIBUTOR",
"HDINSIGHT_CLUSTER_ADMIN",
"HDINSIGHT_CLUSTER_OPERATOR",
"HDINSIGHT_DOMAIN_SERVICES_CONTRIBUTOR",
"HDINSIGHT_ON_AKS_CLUSTER_ADMIN",
"HDINSIGHT_ON_AKS_CLUSTER_OPERATOR",
"HDINSIGHT_ON_AKS_CLUSTER_POOL_ADMIN",
"HEALTHCARE_AGENT_ADMIN",
"HEALTHCARE_AGENT_EDITOR",
"HEALTHCARE_AGENT_READER",
"HEALTH_SAFEGUARDS_DATA_USER",
"HIERARCHY_SETTINGS_ADMINISTRATOR",
"HYBRIDCOMPUTE_MACHINE_LISTACCESSDETAILS_ACTION_IN_BUILT_ROLE",
"HYBRID_SERVER_ONBOARDING",
"HYBRID_SERVER_RESOURCE_ADMINISTRATOR",
"IMPACT_READER",
"IMPACT_REPORTER",
"INTEGRATION_SERVICE_ENVIRONMENT_CONTRIBUTOR",
"INTEGRATION_SERVICE_ENVIRONMENT_DEVELOPER",
"INTELLIGENT_SYSTEMS_ACCOUNT_CONTRIBUTOR",
"IOT_HUB_DATA_CONTRIBUTOR",
"IOT_HUB_DATA_READER",
"IOT_HUB_REGISTRY_CONTRIBUTOR",
"IOT_HUB_TWIN_CONTRIBUTOR",
"IPAM_POOL_USER",
"ISSUE_CONTRIBUTOR",
"KEY_VAULT_ADMINISTRATOR",
"KEY_VAULT_CERTIFICATES_OFFICER",
"KEY_VAULT_CERTIFICATE_USER",
"KEY_VAULT_CONTRIBUTOR",
"KEY_VAULT_CRYPTO_OFFICER",
"KEY_VAULT_CRYPTO_SERVICE_ENCRYPTION_USER",
"KEY_VAULT_CRYPTO_SERVICE_RELEASE_USER",
"KEY_VAULT_CRYPTO_USER",
"KEY_VAULT_DATA_ACCESS_ADMINISTRATOR",
"KEY_VAULT_PURGE_OPERATOR",
"KEY_VAULT_READER",
"KEY_VAULT_SECRETS_OFFICER",
"KEY_VAULT_SECRETS_USER",
"KNOWLEDGE_CONSUMER",
"KOSTNER_DFS_BASE_ROLE",
"KUBERNETESRUNTIME_LOAD_BALANCER_CONTRIBUTOR_ROLE",
"KUBERNETES_AGENTLESS_OPERATOR",
"KUBERNETES_AGENT_OPERATOR",
"KUBERNETES_AGENT_SUBSCRIPTION_LEVEL_OPERATOR",
"KUBERNETES_CLUSTER_AZURE_ARC_ONBOARDING",
"KUBERNETES_EXTENSION_CONTRIBUTOR",
"KUBERNETES_NAMESPACE_USER",
"KUBERNETES_RUNTIME_STORAGE_CLASS_CONTRIBUTOR_ROLE",
"LAB_ASSISTANT",
"LAB_CONTRIBUTOR",
"LAB_CREATOR",
"LAB_OPERATOR",
"LAB_SERVICES_CONTRIBUTOR",
"LAB_SERVICES_READER",
"LANDING_ZONE_ACCOUNT_OWNER",
"LANDING_ZONE_ACCOUNT_READER",
"LANDING_ZONE_MANAGEMENT_OWNER",
"LANDING_ZONE_MANAGEMENT_READER",
"LOAD_TEST_CONTRIBUTOR",
"LOAD_TEST_OWNER",
"LOAD_TEST_READER",
"LOCALNGFIREWALLADMINISTRATOR_ROLE",
"LOCALRULESTACKSADMINISTRATOR_ROLE",
"LOCKS_CONTRIBUTOR",
"LOGIC_APPS_STANDARD_CONTRIBUTOR_PREVIEW",
"LOGIC_APPS_STANDARD_DEVELOPER_PREVIEW",
"LOGIC_APPS_STANDARD_OPERATOR_PREVIEW",
"LOGIC_APPS_STANDARD_READER_PREVIEW",
"LOGIC_APP_CONTRIBUTOR",
"LOGIC_APP_OPERATOR",
"LOG_ANALYTICS_CONTRIBUTOR",
"LOG_ANALYTICS_READER",
"MANAGED_APPLICATION_CONTRIBUTOR_ROLE",
"MANAGED_APPLICATION_OPERATOR_ROLE",
"MANAGED_APPLICATION_PUBLISHER_OPERATOR",
"MANAGED_HSM_CONTRIBUTOR",
"MANAGED_IDENTITY_CONTRIBUTOR",
"MANAGED_IDENTITY_FEDERATED_IDENTITY_CREDENTIAL_CONTRIBUTOR",
"MANAGED_IDENTITY_OPERATOR",
"MANAGED_SERVICES_REGISTRATION_ASSIGNMENT_DELETE_ROLE",
"MANAGEMENT_GROUP_CONTRIBUTOR",
"MANAGEMENT_GROUP_READER",
"MEDIA_SERVICES_ACCOUNT_ADMINISTRATOR",
"MEDIA_SERVICES_LIVE_EVENTS_ADMINISTRATOR",
"MEDIA_SERVICES_MEDIA_OPERATOR",
"MEDIA_SERVICES_POLICY_ADMINISTRATOR",
"MEDIA_SERVICES_STREAMING_ENDPOINTS_ADMINISTRATOR",
"MICROSOFT_EDGE_WINFIELDS_FEDERATED_SUBSCRIPTION_READ_ACCESS_ROLE",
"MICROSOFT_KUBERNETES_CONNECTED_CLUSTER_ROLE",
"MICROSOFT_POWERBI_TENANT_OPERATIONS_ROLE",
"MICROSOFT_SENTINEL_AUTOMATION_CONTRIBUTOR",
"MICROSOFT_SENTINEL_BUSINESS_APPLICATIONS_AGENT_OPERATOR",
"MICROSOFT_SENTINEL_CONTRIBUTOR",
"MICROSOFT_SENTINEL_PLAYBOOK_OPERATOR",
"MICROSOFT_SENTINEL_READER",
"MICROSOFT_SENTINEL_RESPONDER",
"MONITORING_CONTRIBUTOR",
"MONITORING_DATA_READER",
"MONITORING_METRICS_PUBLISHER",
"MONITORING_READER",
"MYSQL_BACKUP_AND_EXPORT_OPERATOR",
"NETWORK_CONTRIBUTOR",
"NEW_RELIC_APM_ACCOUNT_CONTRIBUTOR",
"NEXUS_NETWORK_FABRIC_SERVICE_READER",
"NEXUS_NETWORK_FABRIC_SERVICE_WRITER",
"OBJECT_ANCHORS_ACCOUNT_OWNER",
"OBJECT_ANCHORS_ACCOUNT_READER",
"OBJECT_UNDERSTANDING_ACCOUNT_OWNER",
"OBJECT_UNDERSTANDING_ACCOUNT_READER",
"ONLINE_EXPERIMENTATION_CONTRIBUTOR",
"ONLINE_EXPERIMENTATION_DATA_OWNER",
"ONLINE_EXPERIMENTATION_DATA_READER",
"ONLINE_EXPERIMENTATION_READER",
"OPENCOST_RATE_CARD_READER",
"OPERATOR_NEXUS_COMPUTE_CONTRIBUTOR_ROLE_PREVIEW",
"OPERATOR_NEXUS_KEY_VAULT_WRITER_SERVICE_ROLE_PREVIEW",
"OPERATOR_NEXUS_OWNER_PREVIEW",
"ORACLE_DATABASE_AUTONOMOUS_DATABASE_ADMINISTRATOR",
"ORACLE_DATABASE_EXADATA_INFRASTRUCTURE_ADMINISTRATOR_BUILT_IN_ROLE",
"ORACLE_DATABASE_EXASCALE_STORAGE_VAULT_ADMINISTRATOR",
"ORACLE_DATABASE_EXASCALE_VMCLUSTER_ADMINISTRATOR",
"ORACLE_DATABASE_OWNER_BUILT_IN_ROLE",
"ORACLE_DATABASE_READER_BUILT_IN_ROLE",
"ORACLE_DATABASE_VMCLUSTER_ADMINISTRATOR_BUILT_IN_ROLE",
"ORACLE_SUBSCRIPTIONS_MANAGER_BUILT_IN_ROLE",
"OWNER",
"PLAYFAB_CONTRIBUTOR",
"PLAYFAB_READER",
"POLICY_INSIGHTS_DATA_WRITER_PREVIEW",
"PORTAL_DASHBOARD_WRITER_SERVICE_ROLE",
"POSTGRESQL_FLEXIBLE_SERVER_LONG_TERM_RETENTION_BACKUP_ROLE",
"POWER_PLATFORM_ACCOUNT_CONTRIBUTOR",
"POWER_PLATFORM_ENTERPRISE_POLICY_CONTRIBUTOR",
"PRIVATE_DNS_ZONE_CONTRIBUTOR",
"PROCUREMENT_CONTRIBUTOR",
"PROJECT_BABYLON_DATA_CURATOR",
"PROJECT_BABYLON_DATA_READER",
"PROJECT_BABYLON_DATA_SOURCE_ADMINISTRATOR",
"PROVIDERHUB_CONTRIBUTOR",
"PROVIDERHUB_READER",
"QUANTUM_WORKSPACE_DATA_CONTRIBUTOR",
"QUOTA_REQUEST_OPERATOR",
"READER",
"READER_AND_DATA_ACCESS",
"REDIS_CACHE_CONTRIBUTOR",
"REMOTE_RENDERING_ADMINISTRATOR",
"REMOTE_RENDERING_CLIENT",
"RESERVATION_PURCHASER",
"RESOURCE_POLICY_CONTRIBUTOR",
"ROLE_BASED_ACCESS_CONTROL_ADMINISTRATOR",
"SAAS_HUB_CONTRIBUTOR",
"SAVINGS_PLAN_PURCHASER",
"SCHEDULED_EVENTS_CONTRIBUTOR",
"SCHEDULED_PATCHING_CONTRIBUTOR",
"SCHEDULER_JOB_COLLECTIONS_CONTRIBUTOR",
"SCHEMA_REGISTRY_CONTRIBUTOR",
"SCHEMA_REGISTRY_READER",
"SEARCH_INDEX_DATA_CONTRIBUTOR",
"SEARCH_INDEX_DATA_READER",
"SEARCH_PARAMETER_MANAGER",
"SEARCH_SERVICE_CONTRIBUTOR",
"SECRETS_STORE_EXTENSION_OWNER",
"SECURE_SCORE_READER",
"SECURITY_ADMIN",
"SECURITY_ASSESSMENT_CONTRIBUTOR",
"SECURITY_DETONATION_CHAMBER_PUBLISHER",
"SECURITY_DETONATION_CHAMBER_READER",
"SECURITY_DETONATION_CHAMBER_SUBMISSION_MANAGER",
"SECURITY_DETONATION_CHAMBER_SUBMITTER",
"SECURITY_MANAGER_LEGACY",
"SECURITY_READER",
"SERVICES_HUB_OPERATOR",
"SERVICE_CONNECTOR_CONTRIBUTOR",
"SERVICE_FABRIC_CLUSTER_CONTRIBUTOR",
"SERVICE_FABRIC_MANAGED_CLUSTER_CONTRIBUTOR",
"SERVICE_GROUP_READER",
"SIGNALR_ACCESSKEY_READER",
"SIGNALR_APP_SERVER",
"SIGNALR_REST_API_OWNER",
"SIGNALR_REST_API_READER",
"SIGNALR_SERVICE_OWNER",
"SIGNALR_WEB_PUBSUB_CONTRIBUTOR",
"SITE_RECOVERY_CONTRIBUTOR",
"SITE_RECOVERY_OPERATOR",
"SITE_RECOVERY_READER",
"SPATIALMAPSACCOUNTS_ACCOUNT_OWNER",
"SPATIAL_ANCHORS_ACCOUNT_CONTRIBUTOR",
"SPATIAL_ANCHORS_ACCOUNT_OWNER",
"SPATIAL_ANCHORS_ACCOUNT_READER",
"SQLDB_MIGRATION_ROLE",
"SQLMI_MIGRATION_ROLE",
"SQLVM_MIGRATION_ROLE",
"SQL_DB_CONTRIBUTOR",
"SQL_MANAGED_INSTANCE_CONTRIBUTOR",
"SQL_SECURITY_MANAGER",
"SQL_SERVER_CONTRIBUTOR",
"SSH_PUBLICKEYS_CONTRIBUTOR_ROLE",
"SSH_PUBLICKEYS_READER_ROLE",
"STANDBY_CONTAINER_GROUP_POOL_CONTRIBUTOR",
"STORAGE_ACCOUNT_BACKUP_CONTRIBUTOR",
"STORAGE_ACCOUNT_CONTRIBUTOR",
"STORAGE_ACCOUNT_ENCRYPTION_SCOPE_CONTRIBUTOR_ROLE",
"STORAGE_ACCOUNT_KEY_OPERATOR_SERVICE_ROLE",
"STORAGE_BLOB_DATA_CONTRIBUTOR",
"STORAGE_BLOB_DATA_OWNER",
"STORAGE_BLOB_DATA_READER",
"STORAGE_BLOB_DELEGATOR",
"STORAGE_FILE_DATA_PRIVILEGED_CONTRIBUTOR",
"STORAGE_FILE_DATA_PRIVILEGED_READER",
"STORAGE_FILE_DATA_SMB_SHARE_CONTRIBUTOR",
"STORAGE_FILE_DATA_SMB_SHARE_ELEVATED_CONTRIBUTOR",
"STORAGE_FILE_DATA_SMB_SHARE_READER",
"STORAGE_QUEUE_DATA_CONTRIBUTOR",
"STORAGE_QUEUE_DATA_MESSAGE_PROCESSOR",
"STORAGE_QUEUE_DATA_MESSAGE_SENDER",
"STORAGE_QUEUE_DATA_READER",
"STORAGE_TABLE_DATA_CONTRIBUTOR",
"STORAGE_TABLE_DATA_READER",
"STREAM_ANALYTICS_CONTRIBUTOR",
"STREAM_ANALYTICS_QUERY_TESTER",
"STREAM_ANALYTICS_READER",
"SUBSCRIPTION_CREATOR",
"SUBSCRIPTION_CREATOR_OLD",
"SUPPORT_REQUEST_CONTRIBUTOR",
"TAG_CONTRIBUTOR",
"TAG_READER",
"TEMPLATE_SPEC_CONTRIBUTOR",
"TEMPLATE_SPEC_READER",
"TEST_BASE_READER",
"TOOLCHAINORCHESTRATOR_ADMIN_ROLE",
"TOOLCHAINORCHESTRATOR_VIEWER_ROLE",
"TRAFFIC_MANAGER_CONTRIBUTOR",
"TRANSPARENCY_LOGS_OWNER",
"TRUSTED_SIGNING_CERTIFICATE_PROFILE_SIGNER",
"TRUSTED_SIGNING_IDENTITY_VERIFIER",
"USAGE_BILLING_CONTRIBUTOR",
"USER_ACCESS_ADMINISTRATOR",
"VIDEO_INDEXER_RESTRICTED_VIEWER",
"VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN",
"VIRTUAL_MACHINE_CONTRIBUTOR",
"VIRTUAL_MACHINE_DATA_ACCESS_ADMINISTRATOR_PREVIEW",
"VIRTUAL_MACHINE_LOCAL_USER_LOGIN",
"VIRTUAL_MACHINE_USER_LOGIN",
"VM_RESTORE_OPERATOR",
"VM_SCANNER_OPERATOR",
"WEBSITE_CONTRIBUTOR",
"WEB_PLAN_CONTRIBUTOR",
"WEB_PUBSUB_SERVICE_OWNER",
"WEB_PUBSUB_SERVICE_READER",
"WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN",
"WORKBOOK_CONTRIBUTOR",
"WORKBOOK_READER",
"WORKLOADBUILDER_MIGRATION_AGENT_ROLE",
"WORKLOAD_ORCHESTRATION_IT_ADMIN",
"WORKLOAD_ORCHESTRATION_SOLUTION_EXTERNAL_VALIDATOR"
]

View File

@@ -18,20 +18,8 @@ from typing import Any
logger = logging.getLogger(__name__)
# Known Azure enum values not always captured in the catalog schema
def _load_azure_roles() -> list[str]:
"""Load Azure roles from azure_roles.json (generated from bicep/lookup/rbaclookup)."""
roles_file = pathlib.Path(__file__).parent / "azure_roles.json"
if roles_file.exists():
try:
return json.loads(roles_file.read_text())
except Exception as e:
logger.warning("Failed to load azure_roles.json: %s", e)
return []
_KNOWN_ENUMS: dict[str, list[str]] = {
"principalType": ["User", "Group", "ServicePrincipal", "Device", "ForeignGroup"],
"roles": _load_azure_roles(),
"roleDefinitionIds": _load_azure_roles(), # alias for roles
}
# Catalog is baked into the image root at /bicep_modules_catalog.json
@@ -48,6 +36,34 @@ _IAC_SOURCE_PATHS = [
pathlib.Path(__file__).parent.parent.parent / "iac_source_catalog.json", # dev
]
# Principals catalog — known object IDs for array params (e.g. additionalAccess)
# Format: {"params": {"additionalAccess": [{"id": "<guid>", "label": "...", "description": "..."}]}}
_PRINCIPALS_CATALOG_PATHS = [
pathlib.Path("/data/principals_catalog.json"), # volume-mount (freshest)
pathlib.Path("/principals_catalog.json"), # baked into image (fallback)
pathlib.Path(__file__).parent.parent.parent / "principals_catalog.json", # dev
]
def _load_principals_catalog() -> dict[str, list[dict[str, Any]]]:
"""Load principals catalog for array param completions.
Returns dict keyed by param name (e.g. 'additionalAccess') → list of
{id, label, description} entries.
"""
for path in _PRINCIPALS_CATALOG_PATHS:
if path.exists():
try:
data = json.loads(path.read_text())
params = data.get("params", {})
count = sum(len(v) for v in params.values())
logger.info("Principals catalog loaded from %s: %d entries", path, count)
return params
except Exception:
logger.exception("Failed to parse principals catalog at %s", path)
logger.debug("No principals_catalog.json found — array param completions unavailable")
return {}
def _load_iac_source_catalog() -> dict[str, dict[str, Any]]:
"""Load IAC source catalog for enriched param descriptions.
@@ -106,12 +122,14 @@ class BicepModuleCatalog:
_modules: list[dict[str, Any]] = []
_iac: dict[str, dict[str, Any]] = {} # module name → IAC source info
_principals: dict[str, list[dict[str, Any]]] = {} # param name → [{id, label, description}]
@classmethod
def load(cls) -> None:
"""Load both catalogs from disk. Call once at startup."""
"""Load all catalogs from disk. Call once at startup."""
cls._modules = _load_catalog()
cls._iac = _load_iac_source_catalog()
cls._principals = _load_principals_catalog()
@classmethod
def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]:
@@ -258,6 +276,44 @@ class BicepModuleCatalog:
})
return items
@classmethod
def param_array_item_completion_items(
cls,
module_name: str,
version: str,
param_name: str,
has_open_quote: bool = False,
) -> list[dict[str, Any]]:
"""Completions for items inside an array param (e.g. additionalAccess objectIds).
Looks up known entries from the principals catalog keyed by param name.
"""
entries = cls._principals.get(param_name, [])
if not entries:
return []
items = []
for i, entry in enumerate(entries):
val = entry["id"]
label = entry.get("label", val)
description = entry.get("description", "")
insert = f"{val}'" if has_open_quote else f"'{val}'"
doc = f"**{label}**\n\n`{val}`"
if description:
doc += f"\n\n{description}"
items.append({
"label": label,
"kind": 12, # Value
"detail": val, # GUID shown as detail
"insertText": insert,
"sortText": f"0_lru_arr_{i:03d}_{label}",
"documentation": {
"kind": "markdown",
"value": doc,
},
})
return items
@classmethod
def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]:
"""Param completions for a specific module+version combination."""
@@ -280,56 +336,6 @@ class BicepModuleCatalog:
iac_params = cls._iac_param_map(module_name)
items = []
# Build snippet for full params block (shown first)
snippet_params = []
tabstop = 1
for param_name, param_info in ver_params.items():
iac = iac_params.get(param_name, {})
required = iac.get("required", False)
ptype = param_info.get("type", "any")
allowed = param_info.get("allowed", [])
# Include required params + first few optional params in snippet
if required or len(snippet_params) < 5:
if allowed:
# Enum: use placeholder with first allowed value
placeholder = f"'{allowed[0]}'"
elif ptype == "bool":
placeholder = "true"
elif ptype == "int":
placeholder = "0"
elif ptype == "array":
placeholder = "[]"
elif ptype == "object":
placeholder = "{{}}"
else:
placeholder = "''"
snippet_params.append(f" {param_name}: ${{{tabstop}:{placeholder}}}")
tabstop += 1
if snippet_params:
snippet_text = "\n" + "\n".join(snippet_params) + "\n"
required_count = sum(1 for p, i in ver_params.items()
if iac_params.get(p, {}).get("required", False))
items.append({
"label": "⚡ Fill params block",
"kind": 15, # Snippet
"detail": f"{len(snippet_params)} params ({required_count} required)",
"insertText": snippet_text,
"insertTextFormat": 2, # Snippet
"sortText": "0_lru_snippet_000",
"documentation": {
"kind": "markdown",
"value": (
f"**Fill params block**\n\n"
f"Inserts {len(snippet_params)} params for `{module_name}`.\n"
f"Use Tab to navigate between fields."
),
},
})
# Individual param completions
for param_name, param_info in ver_params.items():
ptype = param_info.get("type", "any")
allowed = param_info.get("allowed", [])

View File

@@ -130,6 +130,19 @@ class _ProxySession:
mod_name = last_mod.group(1)
mod_ver = last_mod.group(2)
# Array item context: cursor inside [...] for an array param.
# Must be checked BEFORE value_m, since array lines also match
# the value pattern (e.g. `additionalAccess: ['`).
array_m = re.search(r"^\s*(\w+):\s*\[", current)
if array_m and current.count("[") > current.count("]"):
return {
"type": "param_array_item",
"module": mod_name,
"version": mod_ver,
"param": array_m.group(1),
"has_open_quote": bool(re.search(r"'[^']*$", current)),
}
# Check if cursor is after 'paramname: ' on the current line
# (value context — inject enum/allowed values)
value_m = re.search(r"^\s*(\w+):\s*('?)([^'{}]*)$", current)
@@ -142,43 +155,6 @@ class _ProxySession:
"has_open_quote": bool(value_m.group(2)),
}
# Check if cursor is inside an array value for a param
# e.g. "roles: ['KEY_VAULT_" or "roles: [ '"
array_m = re.search(r"^\s*(\w+):\s*\[[^\]]*?('?)([^',\]]*)$", current)
if array_m and array_m.group(1) not in {"params", "name", "module", "resource"}:
return {
"type": "param_value",
"module": mod_name,
"version": mod_ver,
"param": array_m.group(1),
"has_open_quote": bool(array_m.group(2)),
}
# Check if cursor is inside a multi-line array element
# e.g. " 'APP_CONFIG" on line after "roles: ["
# Walk backwards to find the array opening
for lookback_idx in range(line_idx - 1, max(0, line_idx - 10), -1):
prev_line = lines[lookback_idx]
# Found array opening like "roles: [" or " roles: ["
array_open_m = re.match(r"^\s*(\w+):\s*\[$", prev_line.rstrip())
if array_open_m:
param_name = array_open_m.group(1)
if param_name not in {"params", "name", "module", "resource"}:
# Check if current line is inside the array (has quote or is indented)
if re.match(r"^\s+('?)([^',\]]*)\s*$", current):
has_quote = bool(re.match(r"^\s+'", current))
return {
"type": "param_value",
"module": mod_name,
"version": mod_ver,
"param": param_name,
"has_open_quote": has_quote,
}
break
# Stop if we hit a closing bracket (we're outside the array)
if "]" in prev_line:
break
return {
"type": "param",
"module": mod_name,
@@ -234,11 +210,18 @@ def _inject_completions(msg: dict[str, Any], context: dict | None = None) -> byt
context["param"],
context.get("has_open_quote", False),
)
elif ctx_type == "param_array_item":
lru_items = BicepModuleCatalog.param_array_item_completion_items(
context["module"],
context["version"],
context["param"],
context.get("has_open_quote", False),
)
else:
# Default: module name completions
lru_items = BicepModuleCatalog.as_completion_items()
if ctx_type in ("version", "param", "param_value"):
if ctx_type in ("version", "param", "param_value", "param_array_item"):
# Always replace LS completions for private-registry contexts — the
# Bicep LS doesn't know about our ACR, so anything it returns is noise.
# Even if lru_items is empty (no enum values for a param), suppress LS.

11
principals_catalog.json Normal file
View File

@@ -0,0 +1,11 @@
{
"params": {
"additionalAccess": [
{
"id": "c88bf29d-b13a-4153-9738-8995085a451e",
"label": "LRIADMPRO-IaC-Bicep",
"description": "IaC Bicep pipeline service principal"
}
]
}
}

View File

@@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""
Extract all Azure role names from rbacLookup.bicep and generate Python code
for _KNOWN_ENUMS in modules.py.
Usage:
python3 scripts/extract_roles_from_rbaclookup.py /path/to/rbacLookup.bicep
"""
import re
import sys
from pathlib import Path
def extract_role_names(bicep_file: Path) -> list[str]:
"""Extract all role names from the rbacLookup.bicep var roles = {...} block."""
content = bicep_file.read_text()
# Find the "var roles = {" block
roles_match = re.search(r'@export\(\)\s*var\s+roles\s*=\s*\{(.+?)\n\}', content, re.DOTALL)
if not roles_match:
raise ValueError("Could not find 'var roles = {' block in Bicep file")
roles_block = roles_match.group(1)
# Extract all role names (keys before the colon)
# Pattern: " ROLE_NAME: 'guid'"
role_names = re.findall(r'^\s+([A-Z_]+):', roles_block, re.MULTILINE)
return sorted(role_names)
def generate_python_code(role_names: list[str]) -> str:
"""Generate Python code for _KNOWN_ENUMS["roles"]."""
lines = ['_KNOWN_ENUMS = {']
lines.append(' "roles": [')
for role in role_names:
lines.append(f' "{role}",')
lines.append(' ],')
lines.append(' "roleDefinitionIds": [ # alias for roles')
for role in role_names:
lines.append(f' "{role}",')
lines.append(' ],')
lines.append('}')
return '\n'.join(lines)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: extract_roles_from_rbaclookup.py /path/to/rbacLookup.bicep", file=sys.stderr)
sys.exit(1)
bicep_path = Path(sys.argv[1])
if not bicep_path.exists():
print(f"Error: File not found: {bicep_path}", file=sys.stderr)
sys.exit(1)
try:
roles = extract_role_names(bicep_path)
print(f"# Extracted {len(roles)} roles from {bicep_path.name}")
print(f"# Generated: {Path(__file__).name}\n")
print(generate_python_code(roles))
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)

View File

@@ -17,7 +17,6 @@ frames straight to stdout.
import asyncio
import ssl
import sys
import traceback
import websockets
@@ -72,9 +71,8 @@ async def main(uri: str) -> None:
if msg is None:
break
await ws.send(msg)
except Exception as e:
print(f"stdin_to_ws error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
except Exception:
pass
async def ws_to_stdout() -> None:
try:
@@ -82,9 +80,8 @@ async def main(uri: str) -> None:
data = frame if isinstance(frame, bytes) else frame.encode()
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
except Exception as e:
print(f"ws_to_stdout error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
except Exception:
pass
await asyncio.gather(stdin_to_ws(), ws_to_stdout())
@@ -93,9 +90,4 @@ if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
sys.exit(1)
try:
asyncio.run(main(sys.argv[1]))
except Exception as e:
print(f"Fatal error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
sys.exit(1)

View File

@@ -1,12 +0,0 @@
#!/bin/bash
# Wrapper for lsp_bridge.py that logs to a file for debugging IntelliJ issues
LOG_FILE="/tmp/lsp_bridge_debug.log"
echo "=== LSP Bridge started at $(date) ===" >> "$LOG_FILE"
echo "Args: $@" >> "$LOG_FILE"
echo "PWD: $(pwd)" >> "$LOG_FILE"
echo "Python: $(which python3)" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
exec /opt/homebrew/bin/python3 /Users/lrihni/Projects/iLSP/scripts/lsp_bridge.py "$@" 2>> "$LOG_FILE"

View File

@@ -31,6 +31,7 @@ BICEP_CATALOG="$DEVOPS_MCP_REPO/bicep_modules_catalog.json"
IAC_CATALOG="$DEVOPS_MCP_REPO/iac_source_catalog.json"
ILSP_REPO="$(cd "$(dirname "$0")/.." && pwd)"
TMPL_CATALOG="$ILSP_REPO/pipeline_templates_catalog.json"
PRINCIPALS_CATALOG="$ILSP_REPO/principals_catalog.json"
echo "── iLSP catalog push ──────────────────────────────"
@@ -50,6 +51,13 @@ if [[ ! -f "$TMPL_CATALOG" ]]; then
fi
echo "$(basename "$TMPL_CATALOG") ($(du -sh "$TMPL_CATALOG" | cut -f1))"
if [[ ! -f "$PRINCIPALS_CATALOG" ]]; then
echo " ✗ Not found: $PRINCIPALS_CATALOG"
echo " Run: python3 $ILSP_REPO/scripts/sync_principals_catalog.py"
exit 1
fi
echo "$(basename "$PRINCIPALS_CATALOG") ($(du -sh "$PRINCIPALS_CATALOG" | cut -f1))"
# Copy iac_source_catalog.json to iLSP repo root so it gets baked into the Docker image
echo ""
echo " → Copying iac_source_catalog.json to iLSP repo root (for Docker bake) …"
@@ -59,7 +67,7 @@ echo " ✓ Copied to $ILSP_REPO/iac_source_catalog.json"
echo ""
echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …"
ssh "$AUTOBOX" "mkdir -p $REMOTE_DIR"
scp "$BICEP_CATALOG" "$IAC_CATALOG" "$TMPL_CATALOG" "$AUTOBOX:$REMOTE_DIR/"
scp "$BICEP_CATALOG" "$IAC_CATALOG" "$TMPL_CATALOG" "$PRINCIPALS_CATALOG" "$AUTOBOX:$REMOTE_DIR/"
echo " ✓ Upload done"
if [[ "$NO_RELOAD" == "true" ]]; then
@@ -78,5 +86,5 @@ else
fi
echo ""
echo " Done. Bicep + YAML pipeline template completions updated."
echo " Done. Bicep + YAML pipeline template + principals completions updated."
echo " Note: iac_source_catalog.json was copied to repo root — commit + push to bake into next Docker image."

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
sync_principals_catalog.py — Build principals_catalog.json by scanning .bicep files
for array param values (GUIDs) and their inline comments.
Scans configured IaC repo directories for patterns like:
additionalAccess: ['c88bf29d-...'] // LRIADMPRO-IaC-Bicep
additionalAccess: [
'c88bf29d-...' // LRIADMPRO-IaC-Bicep
'another-guid' // Another-SP
]
Usage:
python3 scripts/sync_principals_catalog.py
python3 scripts/sync_principals_catalog.py --paths ~/IdeaProjects/Bitbucket/IaC
python3 scripts/sync_principals_catalog.py --dry-run
python3 scripts/sync_principals_catalog.py --output /path/to/principals_catalog.json
"""
import argparse
import json
import logging
import pathlib
import re
import sys
from datetime import datetime, timezone
from typing import Any
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger(__name__)
_REPO_ROOT = pathlib.Path(__file__).parent.parent
_DEFAULT_OUTPUT = _REPO_ROOT / "principals_catalog.json"
# Default paths to scan — adjust to match your IaC repo locations
_DEFAULT_SCAN_PATHS = [
"~/IdeaProjects/Bitbucket/IaC",
"~/IdeaProjects/Bitbucket/LRU",
]
# Matches a UUID/GUID
_GUID_RE = re.compile(
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
)
# Matches a single quoted GUID optionally followed by an inline comment:
# 'c88bf29d-...' // Some label text
# 'c88bf29d-...' // or with hash comments
_ITEM_RE = re.compile(
r"'(" + _GUID_RE.pattern + r")'\s*(?://+\s*(.+?)\s*)?$",
re.IGNORECASE,
)
# Matches the opening of an array param assignment:
# additionalAccess: [ or additionalAccess: ['guid'
_ARRAY_OPEN_RE = re.compile(r"^\s*(\w+)\s*:\s*\[")
def _extract_label(comment: str | None) -> str | None:
"""Clean up an inline comment to use as a display label."""
if not comment:
return None
# Strip trailing punctuation and whitespace
return comment.strip().rstrip(".,;")
def scan_file(path: pathlib.Path) -> dict[str, list[dict[str, Any]]]:
"""Scan a single .bicep file and return {param_name: [{id, label, source}]}."""
try:
text = path.read_text(encoding="utf-8")
except Exception as exc:
log.debug("Cannot read %s: %s", path, exc)
return {}
lines = text.splitlines()
results: dict[str, list[dict[str, Any]]] = {}
i = 0
while i < len(lines):
line = lines[i]
array_m = _ARRAY_OPEN_RE.match(line)
if not array_m:
i += 1
continue
param_name = array_m.group(1)
# Collect all characters on this and subsequent lines until array closes
collected = line[array_m.end() - 1:] # from '[' onwards
j = i + 1
# If the array doesn't close on the same line, keep accumulating
while collected.count("[") > collected.count("]") and j < len(lines):
collected += "\n" + lines[j]
j += 1
# Extract all GUID items from the collected block
for item_line in collected.splitlines():
m = _ITEM_RE.search(item_line)
if not m:
continue
guid = m.group(1).lower()
label = _extract_label(m.group(2))
entry: dict[str, Any] = {
"id": guid,
"label": label or guid,
"source": str(path),
}
if label:
entry["description"] = f"From {path.name}"
results.setdefault(param_name, [])
results[param_name].append(entry)
i = j
return results
def scan_paths(paths: list[pathlib.Path]) -> dict[str, list[dict[str, Any]]]:
"""Scan all .bicep files under the given paths, deduplicating GUIDs per param."""
# param_name → {guid → entry} (dict for dedup)
merged: dict[str, dict[str, dict[str, Any]]] = {}
files_scanned = 0
for base in paths:
if not base.exists():
log.warning("Path not found, skipping: %s", base)
continue
for bicep_file in sorted(base.rglob("*.bicep")):
file_results = scan_file(bicep_file)
files_scanned += 1
for param, entries in file_results.items():
bucket = merged.setdefault(param, {})
for entry in entries:
guid = entry["id"]
if guid not in bucket:
bucket[guid] = entry
else:
# Keep the entry with the most informative label
existing = bucket[guid]
if entry.get("label") and entry["label"] != guid:
if existing.get("label") == guid or not existing.get("label"):
bucket[guid] = entry
log.info("Scanned %d .bicep files across %d path(s)", files_scanned, len(paths))
# Flatten back to lists, sorted by label
return {
param: sorted(entries.values(), key=lambda e: e.get("label", e["id"]).lower())
for param, entries in sorted(merged.items())
}
def build_catalog(scan_paths_list: list[pathlib.Path]) -> dict[str, Any]:
params = scan_paths(scan_paths_list)
total = sum(len(v) for v in params.values())
log.info("Found %d unique entries across %d param(s)", total, len(params))
return {
"synced_at": datetime.now(timezone.utc).isoformat(),
"entry_count": total,
"params": params,
}
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--paths",
nargs="+",
default=_DEFAULT_SCAN_PATHS,
metavar="PATH",
help="Directories to scan for .bicep files (default: %(default)s)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print findings without writing the catalog",
)
parser.add_argument(
"--output",
default=str(_DEFAULT_OUTPUT),
help="Output JSON file (default: %(default)s)",
)
args = parser.parse_args()
resolved = [pathlib.Path(p).expanduser().resolve() for p in args.paths]
catalog = build_catalog(resolved)
if args.dry_run:
print(f"\n── Principals catalog (dry-run) ──────────────────────")
if not catalog["params"]:
print(" No GUID values found in .bicep files.")
for param, entries in catalog["params"].items():
print(f"\n param: {param} ({len(entries)} entries)")
for e in entries:
print(f" {e['label']:<40} {e['id']}")
print(f"\n Total: {catalog['entry_count']} entries")
return
out = pathlib.Path(args.output)
# Strip internal 'source' field — not needed at runtime
for entries in catalog["params"].values():
for e in entries:
e.pop("source", None)
out.write_text(json.dumps(catalog, indent=2, ensure_ascii=False), encoding="utf-8")
log.info("Written: %s (%d entries)", out, catalog["entry_count"])
if __name__ == "__main__":
main()

View File

@@ -18,6 +18,7 @@ def test_frame_produces_correct_header():
@pytest.fixture(autouse=True)
def reset_modules():
BicepModuleCatalog._modules = []
BicepModuleCatalog._principals = {}
yield
@@ -173,12 +174,8 @@ def test_param_completions_injected_on_param_context():
assert "roleDefinitionIds" in labels
assert "principalId" in labels
assert "principalType" in labels
# First item is now the snippet completion
assert items[0]["label"] == "⚡ Fill params block"
assert items[0]["kind"] == 15 # Snippet
# Second item should be a regular param
assert items[1]["sortText"].startswith("0_lru_param_")
assert items[1]["kind"] == 5 # Field
assert items[0]["sortText"].startswith("0_lru_param_")
assert items[0]["kind"] == 5 # Field
def test_param_completion_items_have_insert_text():
@@ -186,12 +183,7 @@ def test_param_completion_items_have_insert_text():
"roleassignments", versions=["1.1.x"], schema=_ROLEASSIGNMENTS_SCHEMA
)]
items = BicepModuleCatalog.param_completion_items("roleassignments", "1.1.x")
# First item should be the snippet completion
assert items[0]["label"] == "⚡ Fill params block"
assert items[0]["insertTextFormat"] == 2
assert "principalId" in items[0]["insertText"]
# Individual param items should have ": " suffix
for item in items[1:]:
for item in items:
assert item["insertText"].endswith(": ")
@@ -311,48 +303,6 @@ def test_detect_param_value_context_open_quote():
assert ctx["has_open_quote"] is True
def test_detect_param_value_context_in_array():
"""Cursor inside array value → param_value context."""
lines = [
"module myMod 'br/modules:roleassignments:1.1.x' = {",
" params: {",
" roles: ['KEY_VAULT_", # ← cursor inside array element
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character = len(" roles: ['KEY_VAULT_") = 23
ctx = session._detect_context(URI, {"line": 2, "character": 23})
assert ctx["type"] == "param_value"
assert ctx["module"] == "roleassignments"
assert ctx["param"] == "roles"
assert ctx["has_open_quote"] is True
def test_detect_param_value_context_in_multiline_array():
"""Cursor in multi-line array element → param_value context."""
lines = [
"module myMod 'br/modules:roleassignments:2.0.x' = {",
" params: {",
" assignments: [",
" {",
" roles: [",
" 'APP_CONFIGURATION_", # ← cursor on separate line
" ]",
" }",
" ]",
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character = len(" 'APP_CONFIGURATION_") = 27
ctx = session._detect_context(URI, {"line": 5, "character": 27})
assert ctx["type"] == "param_value"
assert ctx["module"] == "roleassignments"
assert ctx["param"] == "roles"
assert ctx["has_open_quote"] is True
def test_param_value_items_from_catalog_allowed():
"""environmentType completions come from catalog 'allowed' field."""
BicepModuleCatalog._modules = [_make_module(
@@ -404,24 +354,6 @@ def test_param_value_items_known_enum_fallback():
assert "User" in labels
def test_param_value_items_roles_enum():
"""roles parameter uses _KNOWN_ENUMS for Azure role completions."""
BicepModuleCatalog._modules = [_make_module(
"roleassignments",
versions=["1.1.x"],
schema={"1.1.x": {"parameters": {
"roles": {"type": "array"}, # no 'allowed' in catalog
}}},
)]
items = BicepModuleCatalog.param_value_completion_items(
"roleassignments", "1.1.x", "roles"
)
labels = [i["label"] for i in items]
assert "KEY_VAULT_SECRETS_USER" in labels
assert "STORAGE_BLOB_DATA_READER" in labels
assert "CONTRIBUTOR" in labels
def test_param_value_items_empty_for_free_string():
"""A plain string param with no allowed values returns no completions."""
BicepModuleCatalog._modules = [_make_module(
@@ -455,6 +387,111 @@ def test_param_value_injected_in_completion_response():
assert labels == ["DEV", "TEST", "PROD"]
# ── Array item completion tests ────────────────────────────────────────────────
def test_detect_param_array_item_context():
"""Cursor inside [...] for an array param → param_array_item context."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: ['", # ← cursor after opening quote inside array
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character 24 = after the `'` (4 spaces + 16 "additionalAccess" + 2 ": " + 1 "[" + 1 "'" = 24)
ctx = session._detect_context(URI, {"line": 2, "character": 24})
assert ctx["type"] == "param_array_item"
assert ctx["module"] == "modules/keyvault"
assert ctx["param"] == "additionalAccess"
assert ctx["has_open_quote"] is True
def test_detect_param_array_item_context_no_quote():
"""Cursor at start of empty array → param_array_item, no open quote."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: [", # ← cursor after [
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
ctx = session._detect_context(URI, {"line": 2, "character": 23})
assert ctx["type"] == "param_array_item"
assert ctx["has_open_quote"] is False
def test_detect_param_array_item_context_after_existing_item():
"""Cursor after an existing item inside array → still param_array_item."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: ['c88bf29d-b13a-4153-9738-8995085a451e', '",
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
char = len(" additionalAccess: ['c88bf29d-b13a-4153-9738-8995085a451e', '")
ctx = session._detect_context(URI, {"line": 2, "character": char})
assert ctx["type"] == "param_array_item"
assert ctx["has_open_quote"] is True
def test_param_array_item_completion_items():
"""param_array_item_completion_items returns entries from principals catalog."""
BicepModuleCatalog._principals = {
"additionalAccess": [
{"id": "aaaa-bbbb", "label": "My-SP", "description": "Test SP"},
]
}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess"
)
assert len(items) == 1
assert items[0]["label"] == "My-SP"
assert items[0]["detail"] == "aaaa-bbbb"
assert items[0]["insertText"] == "'aaaa-bbbb'"
def test_param_array_item_completion_items_open_quote():
BicepModuleCatalog._principals = {
"additionalAccess": [{"id": "aaaa-bbbb", "label": "My-SP"}]
}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess", has_open_quote=True
)
assert items[0]["insertText"] == "aaaa-bbbb'"
def test_param_array_item_empty_when_no_catalog_entry():
BicepModuleCatalog._principals = {}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess"
)
assert items == []
def test_param_array_item_injected_in_completion_response():
"""Full pipeline: param_array_item context injects principal completions."""
BicepModuleCatalog._principals = {
"additionalAccess": [{"id": "aaaa-1111", "label": "IaC-SP", "description": "Pipeline SP"}]
}
msg = _completion_response([{"label": "noise", "sortText": "z"}])
ctx = {
"type": "param_array_item",
"module": "modules/keyvault",
"version": "2.1.x",
"param": "additionalAccess",
"has_open_quote": False,
}
out = json.loads(_inject_completions(msg, ctx))
items = out["result"]["items"]
assert len(items) == 1
assert items[0]["label"] == "IaC-SP"
assert items[0]["detail"] == "aaaa-1111"
def test_detect_unknown_context_outside_module():
lines = ["var x = 'hello'"]
session = _make_session_with_doc(URI, lines)