Compare commits
1 Commits
main
...
backup/mac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0025043999 |
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
@@ -75,20 +43,13 @@ tail -f /tmp/lsp_bridge_debug.log
|
||||
|
||||
## Neovim setup
|
||||
|
||||
> **Important**: `vim.lsp.rpc.connect(url)` does **not** speak WebSocket — it's a raw
|
||||
> TCP/pipe connector and will fail with `ENOENT` on a `wss://` URL (verified). Use
|
||||
> `cmd = { ".../scripts/lsp_bridge_debug.sh", "wss://..." }` instead (stdio ↔
|
||||
> WebSocket bridge, same one IntelliJ/LSP4IJ uses). Confirmed working end-to-end
|
||||
> (32 module completions returned) with the config below.
|
||||
|
||||
```lua
|
||||
-- In your LSP config (e.g. ~/.config/nvim/lua/lsp.lua)
|
||||
local BRIDGE = "/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh"
|
||||
|
||||
-- Bicep
|
||||
vim.lsp.start({
|
||||
name = "ilsp-bicep",
|
||||
cmd = { BRIDGE, "wss://ilsp.i80.dk/bicep" },
|
||||
cmd = vim.lsp.rpc.connect("wss://ilsp.i80.dk/bicep"),
|
||||
root_dir = vim.fs.dirname(vim.fs.find({ "bicepconfig.json", ".git" }, { upward = true })[1]),
|
||||
filetypes = { "bicep" },
|
||||
})
|
||||
@@ -96,7 +57,7 @@ vim.lsp.start({
|
||||
-- YAML (pipeline files)
|
||||
vim.lsp.start({
|
||||
name = "ilsp-yaml",
|
||||
cmd = { BRIDGE, "wss://ilsp.i80.dk/yaml" },
|
||||
cmd = vim.lsp.rpc.connect("wss://ilsp.i80.dk/yaml"),
|
||||
root_dir = vim.fs.dirname(vim.fs.find({ ".git" }, { upward = true })[1]),
|
||||
filetypes = { "yaml" },
|
||||
})
|
||||
@@ -104,7 +65,7 @@ vim.lsp.start({
|
||||
-- Python
|
||||
vim.lsp.start({
|
||||
name = "ilsp-python",
|
||||
cmd = { BRIDGE, "wss://ilsp.i80.dk/python" },
|
||||
cmd = vim.lsp.rpc.connect("wss://ilsp.i80.dk/python"),
|
||||
root_dir = vim.fs.dirname(vim.fs.find({ "pyproject.toml", ".git" }, { upward = true })[1]),
|
||||
filetypes = { "python" },
|
||||
})
|
||||
@@ -189,20 +150,12 @@ python3 scripts/sync_pipeline_templates.py # → pipeline_templates_catalog.js
|
||||
python3 scripts/sync_iac_module_sources.py # → iac_source_catalog.json (from DevOpsMCP)
|
||||
python3 scripts/sync_bicep_modules.py # → bicep_modules_catalog.json
|
||||
|
||||
# 2. Commit + push — Gitea Actions builds the image and Nomad redeploys it
|
||||
git add bicep_modules_catalog.json iac_source_catalog.json pipeline_templates_catalog.json
|
||||
git commit -m "chore: refresh catalogs"
|
||||
git push
|
||||
# 2. Push and deploy
|
||||
bash scripts/push_catalogs.sh
|
||||
```
|
||||
|
||||
> **Note**: `scripts/push_catalogs.sh` and the `/reload` endpoint only take effect if
|
||||
> `/data` is volume-mounted into the running container. **Production (`ilsp.nomad`)
|
||||
> currently has no `/data` mount** — the underlying Nomad client has raw Docker bind
|
||||
> mounts disabled (`volumes are not enabled`), so `push_catalogs.sh` silently pushes
|
||||
> to a path the container never sees, and `/reload` is a no-op there. `/data` only
|
||||
> works for **local dev** via `make run-with-data`. In production, catalog updates
|
||||
> always require a commit + redeploy (as above) until the client is configured with
|
||||
> a proper Nomad `host_volume`.
|
||||
The `/reload` endpoint on the running service reloads catalogs from volume-mounted
|
||||
files if present, so you can update without a full redeploy for pipeline templates.
|
||||
|
||||
---
|
||||
|
||||
|
||||
71
README.md
71
README.md
@@ -13,70 +13,6 @@ Provides smart autocomplete on top of standard LSPs:
|
||||
|
||||
→ See **[EDITOR_SETUP.md](EDITOR_SETUP.md)** for editor configuration and a full feature overview.
|
||||
|
||||
## LSP feature coverage
|
||||
|
||||
Full capability audit against production (verified 2026-08-14) — everything below
|
||||
is confirmed working end-to-end over the live WebSocket endpoints, not just in
|
||||
theory:
|
||||
|
||||
| Feature | Bicep (`/bicep`) | Python (`/python`) | YAML (`/yaml`) |
|
||||
|---|---|---|---|
|
||||
| Completions (+ internal catalogs) | ✅ | ✅ | ✅ |
|
||||
| Hover | ✅ | ✅ | ✅ |
|
||||
| Go to definition | ✅ | ✅ | — |
|
||||
| Find references | ✅ | ✅ | — |
|
||||
| Document symbols | ✅ | ✅ | — |
|
||||
| Document formatting | ✅ | ✅ | — |
|
||||
| Code actions | ✅ | ✅ | — |
|
||||
| Signature help | ✅ *(iLSP-provided)* | ✅ | — |
|
||||
| Folding range | ✅ *(iLSP-provided)* | ✅ | — |
|
||||
| Workspace symbol | ✅ *(iLSP-provided)* | ❌ (pylsp doesn't implement it) | — |
|
||||
| Diagnostics | ✅ | ✅ | ✅ |
|
||||
|
||||
Bicep.LangServer itself (v0.46.1, latest) doesn't implement signature help,
|
||||
folding range, or workspace symbol — iLSP answers these three locally in the
|
||||
proxy (`ilsp/bicep_lsp/local_features.py`) instead of forwarding to a backend
|
||||
that would just error or return nothing.
|
||||
|
||||
### Confirmed examples (live against `wss://ilsp.i80.dk/bicep`)
|
||||
|
||||
**Role-array completion** — cursor inside `roles: [...]` returns all 682 Azure
|
||||
built-in roles:
|
||||
```
|
||||
roles: ['KEY_VAULT_SECRETS_USER', 'STORAGE_BLOB_DATA_CONTRIBUTOR', <cursor>]
|
||||
→ 682 items: ACCESS_REVIEW_OPERATOR_SERVICE_ROLE, ACRDELETE, ACRIMAGESIGNER, ACRPULL, ACRPUSH, ...
|
||||
```
|
||||
|
||||
**Go to definition** — jumping from a variable usage to its declaration:
|
||||
```bicep
|
||||
var storageName = '${projectName}storage' # ← declared here (line 2)
|
||||
resource sa '...' = {
|
||||
name: storageName # ← cursor here → jumps to line 2
|
||||
}
|
||||
```
|
||||
|
||||
**Find references** — from the `storageName` declaration, finds both places
|
||||
it's used: the string interpolation `${projectName}storage` isn't a reference,
|
||||
but the `resource sa { name: storageName }` property assignment is — returns
|
||||
the declaration itself plus that 1 real usage (2 results total).
|
||||
|
||||
**Document symbols** — outline for a file with params/vars/resources/modules:
|
||||
```
|
||||
location, projectName, storageName, sa, roleAssignment, storageId
|
||||
```
|
||||
|
||||
**Signature help** — cursor right after `resourceId(`:
|
||||
```
|
||||
resourceId(resourceType, resourceName1, resourceName2...)
|
||||
^ active parameter highlighted
|
||||
```
|
||||
|
||||
**Folding range** — every multi-line `{}`/`[]`/`()` block returned as a
|
||||
collapsible region (nested blocks included).
|
||||
|
||||
**Workspace symbol** — searching `"storage"` across all open documents in the
|
||||
editor session returns `storageName`, `storageId`, etc.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
@@ -95,14 +31,9 @@ make health # curl health endpoint
|
||||
|
||||
## Updating catalogs
|
||||
|
||||
Catalogs are baked into the Docker image at build time — commit + push to update
|
||||
production (see [EDITOR_SETUP.md](EDITOR_SETUP.md#updating-catalogs) for the full
|
||||
workflow and a note on why `push_catalogs.sh` / `/reload` don't take effect in prod).
|
||||
|
||||
```bash
|
||||
python3 scripts/sync_pipeline_templates.py # scan AzDO + GHA template repos
|
||||
python3 scripts/sync_bicep_modules.py # from DevOpsMCP repo — needs az CLI + ACR access
|
||||
git add *_catalog.json && git commit -m "chore: refresh catalogs" && git push
|
||||
bash scripts/push_catalogs.sh # scp to server + /reload
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
]
|
||||
@@ -1,249 +0,0 @@
|
||||
"""
|
||||
Curated signature database for Bicep's built-in functions.
|
||||
|
||||
Bicep.LangServer (as of 0.46.1) does not implement textDocument/signatureHelp,
|
||||
so iLSP provides its own for the most commonly used built-in functions.
|
||||
Not exhaustive — covers the functions people actually type day to day.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
# name -> list of parameter labels (one entry per overload, simplest form first)
|
||||
BUILTIN_FUNCTIONS: dict[str, dict[str, Any]] = {
|
||||
"resourceId": {
|
||||
"params": ["resourceType", "resourceName1", "resourceName2..."],
|
||||
"doc": "Returns the unique identifier of a resource. Also usable with a subscriptionId/resourceGroupName prefix.",
|
||||
},
|
||||
"subscriptionResourceId": {
|
||||
"params": ["subscriptionId", "resourceType", "resourceName1", "resourceName2..."],
|
||||
"doc": "Returns the unique identifier for a resource deployed at the subscription level.",
|
||||
},
|
||||
"tenantResourceId": {
|
||||
"params": ["resourceType", "resourceName1", "resourceName2..."],
|
||||
"doc": "Returns the unique identifier for a resource deployed at the tenant level.",
|
||||
},
|
||||
"extensionResourceId": {
|
||||
"params": ["resourceId", "resourceType", "resourceName1", "resourceName2..."],
|
||||
"doc": "Returns the resource ID for an extension resource, which is applied to another resource.",
|
||||
},
|
||||
"guid": {
|
||||
"params": ["baseString1", "baseString2..."],
|
||||
"doc": "Creates a deterministic GUID based on the values passed as parameters.",
|
||||
},
|
||||
"uniqueString": {
|
||||
"params": ["baseString1", "baseString2..."],
|
||||
"doc": "Creates a deterministic hash string based on the values passed as parameters (13 chars).",
|
||||
},
|
||||
"take": {
|
||||
"params": ["originalValue", "count"],
|
||||
"doc": "Returns a string or array with the specified number of elements from the start.",
|
||||
},
|
||||
"skip": {
|
||||
"params": ["originalValue", "count"],
|
||||
"doc": "Returns a string or array with all the elements after the specified number skipped.",
|
||||
},
|
||||
"first": {
|
||||
"params": ["arg1"],
|
||||
"doc": "Returns the first element of an array, or first character of a string.",
|
||||
},
|
||||
"last": {
|
||||
"params": ["arg1"],
|
||||
"doc": "Returns the last element of an array, or last character of a string.",
|
||||
},
|
||||
"length": {
|
||||
"params": ["arg1"],
|
||||
"doc": "Returns the number of elements in an array, characters in a string, or properties in an object.",
|
||||
},
|
||||
"concat": {
|
||||
"params": ["arg1", "arg2..."],
|
||||
"doc": "Combines multiple arrays or strings.",
|
||||
},
|
||||
"union": {
|
||||
"params": ["arg1", "arg2..."],
|
||||
"doc": "Returns a single array/object with all elements from the parameters, merging objects.",
|
||||
},
|
||||
"contains": {
|
||||
"params": ["container", "itemToFind"],
|
||||
"doc": "Checks whether an array contains a value, an object contains a key, or a string contains a substring.",
|
||||
},
|
||||
"empty": {
|
||||
"params": ["itemToTest"],
|
||||
"doc": "Determines if an array, object, or string is empty.",
|
||||
},
|
||||
"format": {
|
||||
"params": ["formatString", "arg1..."],
|
||||
"doc": "Creates a formatted string using .NET-style {0} placeholders.",
|
||||
},
|
||||
"join": {
|
||||
"params": ["inputArray", "delimiter"],
|
||||
"doc": "Joins multiple strings from an array into a single string, separated by delimiter.",
|
||||
},
|
||||
"split": {
|
||||
"params": ["inputString", "delimiter"],
|
||||
"doc": "Splits a string into an array using a delimiter.",
|
||||
},
|
||||
"replace": {
|
||||
"params": ["originalString", "oldString", "newString"],
|
||||
"doc": "Returns a new string with all instances of oldString replaced by newString.",
|
||||
},
|
||||
"toLower": {
|
||||
"params": ["stringToChange"],
|
||||
"doc": "Converts a string to lowercase.",
|
||||
},
|
||||
"toUpper": {
|
||||
"params": ["stringToChange"],
|
||||
"doc": "Converts a string to uppercase.",
|
||||
},
|
||||
"trim": {
|
||||
"params": ["stringToTrim"],
|
||||
"doc": "Removes leading/trailing whitespace from a string.",
|
||||
},
|
||||
"substring": {
|
||||
"params": ["stringToParse", "startIndex", "length"],
|
||||
"doc": "Returns a substring starting at the specified position.",
|
||||
},
|
||||
"padLeft": {
|
||||
"params": ["valueToPad", "totalLength", "paddingCharacter"],
|
||||
"doc": "Pads a string to a fixed total length by adding characters to the left.",
|
||||
},
|
||||
"startsWith": {
|
||||
"params": ["stringToSearch", "stringToFind"],
|
||||
"doc": "Checks whether a string starts with a value (case-insensitive).",
|
||||
},
|
||||
"endsWith": {
|
||||
"params": ["stringToSearch", "stringToFind"],
|
||||
"doc": "Checks whether a string ends with a value (case-insensitive).",
|
||||
},
|
||||
"indexOf": {
|
||||
"params": ["stringToSearch", "stringToFind"],
|
||||
"doc": "Returns the first position of a value within a string (case-insensitive).",
|
||||
},
|
||||
"reference": {
|
||||
"params": ["resourceNameOrIdentifier", "apiVersion", "'Full'"],
|
||||
"doc": "Returns an object representing a resource's runtime state.",
|
||||
},
|
||||
"listKeys": {
|
||||
"params": ["resourceNameOrIdentifier", "apiVersion"],
|
||||
"doc": "Returns the keys for a resource (e.g. storage account access keys).",
|
||||
},
|
||||
"listSecrets": {
|
||||
"params": ["resourceNameOrIdentifier", "apiVersion"],
|
||||
"doc": "Returns the secrets for a resource.",
|
||||
},
|
||||
"resourceGroup": {
|
||||
"params": [],
|
||||
"doc": "Returns an object representing the current resource group.",
|
||||
},
|
||||
"subscription": {
|
||||
"params": [],
|
||||
"doc": "Returns an object representing the current subscription.",
|
||||
},
|
||||
"deployment": {
|
||||
"params": [],
|
||||
"doc": "Returns an object representing the current deployment operation.",
|
||||
},
|
||||
"environment": {
|
||||
"params": [],
|
||||
"doc": "Returns an object describing the Azure cloud environment.",
|
||||
},
|
||||
"az": {
|
||||
"params": [],
|
||||
"doc": "Namespace for Azure Bicep built-in functions.",
|
||||
},
|
||||
"json": {
|
||||
"params": ["jsonString"],
|
||||
"doc": "Converts a valid JSON string into a JSON object / array.",
|
||||
},
|
||||
"string": {
|
||||
"params": ["valueToConvert"],
|
||||
"doc": "Converts the specified value to a string.",
|
||||
},
|
||||
"int": {
|
||||
"params": ["valueToConvert"],
|
||||
"doc": "Converts the specified value to an integer.",
|
||||
},
|
||||
"bool": {
|
||||
"params": ["value"],
|
||||
"doc": "Converts the specified value to a boolean.",
|
||||
},
|
||||
"array": {
|
||||
"params": ["valueToConvert"],
|
||||
"doc": "Converts the specified value to an array.",
|
||||
},
|
||||
"base64": {
|
||||
"params": ["inputString"],
|
||||
"doc": "Returns the base64 representation of the input string.",
|
||||
},
|
||||
"base64ToString": {
|
||||
"params": ["base64Value"],
|
||||
"doc": "Converts a base64 representation to a string.",
|
||||
},
|
||||
"dateTimeAdd": {
|
||||
"params": ["base", "duration", "format"],
|
||||
"doc": "Adds a time duration to a base UTC datetime value.",
|
||||
},
|
||||
"utcNow": {
|
||||
"params": ["format"],
|
||||
"doc": "Returns the current UTC datetime in the specified format.",
|
||||
},
|
||||
"loadTextContent": {
|
||||
"params": ["filePath", "encoding"],
|
||||
"doc": "Loads the content of the specified file as a string at compile time.",
|
||||
},
|
||||
"loadJsonContent": {
|
||||
"params": ["filePath", "jsonPath", "encoding"],
|
||||
"doc": "Loads the content of the specified JSON file at compile time.",
|
||||
},
|
||||
"loadFileAsBase64": {
|
||||
"params": ["filePath"],
|
||||
"doc": "Loads the specified file as a base64 string at compile time.",
|
||||
},
|
||||
"managementGroup": {
|
||||
"params": ["name"],
|
||||
"doc": "Returns an object representing the current or specified management group.",
|
||||
},
|
||||
"newGuid": {
|
||||
"params": [],
|
||||
"doc": "Returns a new (random, non-deterministic) GUID. Only valid for param defaults.",
|
||||
},
|
||||
"filter": {
|
||||
"params": ["array", "lambdaExpression"],
|
||||
"doc": "Filters an array using a custom filtering lambda expression.",
|
||||
},
|
||||
"map": {
|
||||
"params": ["array", "lambdaExpression"],
|
||||
"doc": "Applies a custom mapping lambda expression to each element of an array.",
|
||||
},
|
||||
"reduce": {
|
||||
"params": ["array", "initialValue", "lambdaExpression"],
|
||||
"doc": "Reduces an array with a custom lambda expression.",
|
||||
},
|
||||
"sort": {
|
||||
"params": ["array", "lambdaExpression"],
|
||||
"doc": "Sorts an array with a custom lambda expression comparator.",
|
||||
},
|
||||
"min": {
|
||||
"params": ["arg1", "arg2..."],
|
||||
"doc": "Returns the minimum value from an array of integers, or a comma-separated list.",
|
||||
},
|
||||
"max": {
|
||||
"params": ["arg1", "arg2..."],
|
||||
"doc": "Returns the maximum value from an array of integers, or a comma-separated list.",
|
||||
},
|
||||
"range": {
|
||||
"params": ["startIndex", "count"],
|
||||
"doc": "Creates an array of integers from startIndex to startIndex+count-1.",
|
||||
},
|
||||
"items": {
|
||||
"params": ["object"],
|
||||
"doc": "Returns an array of key/value pair objects that represent the properties of an object.",
|
||||
},
|
||||
"pickZones": {
|
||||
"params": ["providerNamespace", "resourceType", "location", "numberOfZones", "offset"],
|
||||
"doc": "Determines which availability zones are recommended for a resource type/location.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_signature(name: str) -> dict[str, Any] | None:
|
||||
return BUILTIN_FUNCTIONS.get(name)
|
||||
@@ -1,177 +0,0 @@
|
||||
"""
|
||||
Local (non-forwarded) implementations of LSP features that Bicep.LangServer
|
||||
(as of 0.46.1) does not implement itself: textDocument/signatureHelp,
|
||||
textDocument/foldingRange, and workspace/symbol.
|
||||
|
||||
These are intentionally simple, regex/bracket-based implementations — good
|
||||
enough for real editor usage, not a full Bicep parser. They run entirely in
|
||||
the iLSP proxy and never touch the underlying Bicep LS process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from .bicep_functions import get_signature
|
||||
|
||||
_DECL_RE = re.compile(
|
||||
r"^\s*(resource|module|var|param|output|func|type)\s+([A-Za-z_][\w]*)",
|
||||
)
|
||||
|
||||
_SYMBOL_KIND = {
|
||||
"resource": 6, # Method-ish; LSP doesn't have a great "resource" kind, use Variable-family
|
||||
"module": 2, # Module
|
||||
"var": 13, # Variable
|
||||
"param": 13, # Variable
|
||||
"output": 13, # Variable
|
||||
"func": 12, # Function
|
||||
"type": 5, # Class
|
||||
}
|
||||
|
||||
|
||||
# ── signatureHelp ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _find_enclosing_call(current: str) -> tuple[str, int] | None:
|
||||
"""
|
||||
Walk backwards from the cursor to find the nearest unmatched '(' and the
|
||||
identifier immediately preceding it, returning (function_name, arg_index).
|
||||
Returns None if the cursor isn't inside a recognizable function call.
|
||||
"""
|
||||
depth = 0
|
||||
comma_count = 0
|
||||
i = len(current) - 1
|
||||
in_string = False
|
||||
while i >= 0:
|
||||
ch = current[i]
|
||||
if ch == "'" and (i == 0 or current[i - 1] != "\\"):
|
||||
in_string = not in_string
|
||||
elif not in_string:
|
||||
if ch in ")]}":
|
||||
depth += 1
|
||||
elif ch in "(":
|
||||
if depth == 0:
|
||||
# Found the opening paren for the innermost call
|
||||
name_m = re.search(r"([A-Za-z_][\w]*)\s*$", current[:i])
|
||||
if name_m:
|
||||
return name_m.group(1), comma_count
|
||||
return None
|
||||
depth -= 1
|
||||
elif ch in "[{":
|
||||
depth -= 1
|
||||
elif ch == "," and depth == 0:
|
||||
comma_count += 1
|
||||
i -= 1
|
||||
return None
|
||||
|
||||
|
||||
def compute_signature_help(lines: list[str], line: int, character: int) -> dict[str, Any] | None:
|
||||
if line >= len(lines):
|
||||
return None
|
||||
current = lines[line][:character]
|
||||
found = _find_enclosing_call(current)
|
||||
if not found:
|
||||
return None
|
||||
func_name, arg_index = found
|
||||
sig = get_signature(func_name)
|
||||
if not sig:
|
||||
return None
|
||||
|
||||
params = sig["params"]
|
||||
parameters = [{"label": p} for p in params] if params else []
|
||||
active_param = min(arg_index, max(len(parameters) - 1, 0)) if parameters else 0
|
||||
|
||||
label = f"{func_name}({', '.join(params)})"
|
||||
return {
|
||||
"signatures": [
|
||||
{
|
||||
"label": label,
|
||||
"documentation": {"kind": "markdown", "value": sig["doc"]},
|
||||
"parameters": parameters,
|
||||
}
|
||||
],
|
||||
"activeSignature": 0,
|
||||
"activeParameter": active_param,
|
||||
}
|
||||
|
||||
|
||||
# ── foldingRange ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def compute_folding_ranges(lines: list[str]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Simple stack-based bracket matcher over {}, [], () — collapses any
|
||||
multi-line bracket pair into a folding range. Also folds contiguous
|
||||
leading '//' comment blocks.
|
||||
"""
|
||||
ranges: list[dict[str, Any]] = []
|
||||
stack: list[tuple[str, int]] = []
|
||||
in_string = False
|
||||
|
||||
for lineno, text in enumerate(lines):
|
||||
i = 0
|
||||
while i < len(text):
|
||||
ch = text[i]
|
||||
if ch == "'" and (i == 0 or text[i - 1] != "\\"):
|
||||
in_string = not in_string
|
||||
elif not in_string:
|
||||
if ch in "{[(":
|
||||
stack.append((ch, lineno))
|
||||
elif ch in "}])":
|
||||
if stack:
|
||||
_open_ch, open_line = stack.pop()
|
||||
if lineno > open_line:
|
||||
ranges.append({"startLine": open_line, "endLine": lineno, "kind": "region"})
|
||||
i += 1
|
||||
|
||||
# Fold contiguous line-comment blocks (3+ lines)
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
if lines[i].strip().startswith("//"):
|
||||
start = i
|
||||
while i < len(lines) and lines[i].strip().startswith("//"):
|
||||
i += 1
|
||||
if i - start >= 3:
|
||||
ranges.append({"startLine": start, "endLine": i - 1, "kind": "comment"})
|
||||
else:
|
||||
i += 1
|
||||
|
||||
ranges.sort(key=lambda r: (r["startLine"], r["endLine"]))
|
||||
return ranges
|
||||
|
||||
|
||||
# ── workspace/symbol ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def compute_workspace_symbols(docs: dict[str, list[str]], query: str) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Best-effort workspace symbol search over documents currently open in this
|
||||
editor session (iLSP has no full-workspace index — only what the client
|
||||
has opened). Matches top-level resource/module/var/param/output/func/type
|
||||
declarations whose name contains the query (case-insensitive).
|
||||
"""
|
||||
query_l = query.lower()
|
||||
results: list[dict[str, Any]] = []
|
||||
for uri, lines in docs.items():
|
||||
for lineno, text in enumerate(lines):
|
||||
m = _DECL_RE.match(text)
|
||||
if not m:
|
||||
continue
|
||||
kind_word, name = m.group(1), m.group(2)
|
||||
if query_l and query_l not in name.lower():
|
||||
continue
|
||||
start_char = text.index(name, m.end(1))
|
||||
results.append({
|
||||
"name": name,
|
||||
"kind": _SYMBOL_KIND.get(kind_word, 13),
|
||||
"location": {
|
||||
"uri": uri,
|
||||
"range": {
|
||||
"start": {"line": lineno, "character": start_char},
|
||||
"end": {"line": lineno, "character": start_char + len(name)},
|
||||
},
|
||||
},
|
||||
"containerName": kind_word,
|
||||
})
|
||||
return results
|
||||
@@ -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", [])
|
||||
|
||||
@@ -27,11 +27,6 @@ import subprocess
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from .local_features import (
|
||||
compute_folding_ranges,
|
||||
compute_signature_help,
|
||||
compute_workspace_symbols,
|
||||
)
|
||||
from .modules import BicepModuleCatalog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -135,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)
|
||||
@@ -147,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,
|
||||
@@ -192,12 +163,8 @@ class _ProxySession:
|
||||
|
||||
return {"type": "unknown"}
|
||||
|
||||
def pop_context(self, msg_id) -> dict | None:
|
||||
"""Return the tracked completion context for msg_id, or None if this
|
||||
response id does not correspond to a textDocument/completion request
|
||||
we recorded (e.g. hover/definition/references/... responses must never
|
||||
be treated as completion responses)."""
|
||||
return self.pending.pop(msg_id, None)
|
||||
def pop_context(self, msg_id) -> dict:
|
||||
return self.pending.pop(msg_id, {"type": "unknown"})
|
||||
|
||||
|
||||
# ── Completion injection ───────────────────────────────────────────────────────
|
||||
@@ -243,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.
|
||||
@@ -270,26 +244,8 @@ def _inject_completions(msg: dict[str, Any], context: dict | None = None) -> byt
|
||||
return json.dumps(msg).encode()
|
||||
|
||||
|
||||
def _local_response(msg_id, result: Any) -> bytes:
|
||||
return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result}).encode()
|
||||
|
||||
|
||||
class _LockedConn:
|
||||
"""Wraps a socket so concurrent writers (locally-answered responses vs.
|
||||
forwarded LS responses) never interleave partial frames on the wire."""
|
||||
|
||||
def __init__(self, conn: socket.socket, lock: threading.Lock) -> None:
|
||||
self._conn = conn
|
||||
self._lock = lock
|
||||
|
||||
def sendall(self, data: bytes) -> None:
|
||||
with self._lock:
|
||||
self._conn.sendall(data)
|
||||
|
||||
|
||||
def _client_to_ls(
|
||||
conn_file,
|
||||
conn: socket.socket,
|
||||
proc_stdin,
|
||||
session: _ProxySession,
|
||||
) -> None:
|
||||
@@ -299,7 +255,6 @@ def _client_to_ls(
|
||||
logger.debug("Client→LS: %d bytes", len(body))
|
||||
|
||||
# Track document state and completion context (never block forwarding)
|
||||
forward = True
|
||||
try:
|
||||
msg = json.loads(body)
|
||||
method = msg.get("method", "")
|
||||
@@ -315,33 +270,9 @@ def _client_to_ls(
|
||||
session.update_doc(uri, changes[-1].get("text", ""))
|
||||
elif method == "textDocument/completion":
|
||||
session.record_completion_request(msg)
|
||||
elif method == "textDocument/signatureHelp":
|
||||
# Bicep.LangServer doesn't implement this — answer locally.
|
||||
forward = False
|
||||
params = msg.get("params", {})
|
||||
uri = params.get("textDocument", {}).get("uri", "")
|
||||
pos = params.get("position", {})
|
||||
lines = session.docs.get(uri, [])
|
||||
result = compute_signature_help(lines, pos.get("line", 0), pos.get("character", 0))
|
||||
conn.sendall(_frame(_local_response(msg.get("id"), result)))
|
||||
elif method == "textDocument/foldingRange":
|
||||
# Bicep.LangServer doesn't implement this — answer locally.
|
||||
forward = False
|
||||
uri = msg.get("params", {}).get("textDocument", {}).get("uri", "")
|
||||
lines = session.docs.get(uri, [])
|
||||
result = compute_folding_ranges(lines)
|
||||
conn.sendall(_frame(_local_response(msg.get("id"), result)))
|
||||
elif method == "workspace/symbol":
|
||||
# Bicep.LangServer doesn't implement this — answer locally,
|
||||
# scoped to documents open in this editor session.
|
||||
forward = False
|
||||
query = msg.get("params", {}).get("query", "")
|
||||
result = compute_workspace_symbols(session.docs, query)
|
||||
conn.sendall(_frame(_local_response(msg.get("id"), result)))
|
||||
except Exception:
|
||||
forward = True # parsing/handling errors must never block forwarding
|
||||
pass # parsing errors must never block forwarding
|
||||
|
||||
if forward:
|
||||
framed = _frame(body)
|
||||
proc_stdin.write(framed)
|
||||
proc_stdin.flush()
|
||||
@@ -368,26 +299,10 @@ def _ls_to_client(
|
||||
logger.debug("LS→Client: %d bytes", len(body))
|
||||
try:
|
||||
msg = json.loads(body)
|
||||
context = None
|
||||
context: dict = {}
|
||||
if "id" in msg and "result" in msg:
|
||||
context = session.pop_context(msg["id"])
|
||||
# Only rewrite responses that actually correspond to a
|
||||
# textDocument/completion request we tracked — any other
|
||||
# response (hover, definition, references, documentSymbol,
|
||||
# formatting, codeAction, ...) must pass through untouched.
|
||||
if context is not None:
|
||||
out = _inject_completions(msg, context)
|
||||
elif msg.get("method") is None and isinstance(msg.get("result"), dict) and "capabilities" in msg.get("result", {}):
|
||||
# This is the initialize response — advertise the extra
|
||||
# capabilities iLSP answers locally (Bicep.LangServer
|
||||
# itself doesn't implement these).
|
||||
caps = msg["result"]["capabilities"]
|
||||
caps["signatureHelpProvider"] = {"triggerCharacters": ["(", ","]}
|
||||
caps["foldingRangeProvider"] = True
|
||||
caps["workspaceSymbolProvider"] = True
|
||||
out = json.dumps(msg).encode()
|
||||
else:
|
||||
out = body
|
||||
except json.JSONDecodeError:
|
||||
out = body
|
||||
conn.sendall(_frame(out))
|
||||
@@ -411,22 +326,16 @@ def _handle_client(conn: socket.socket, addr: tuple) -> None:
|
||||
# Unbuffered read from the socket — critical for correct LSP framing
|
||||
conn_file = conn.makefile("rb", buffering=0)
|
||||
|
||||
# t1 may write locally-answered responses (signatureHelp/foldingRange/
|
||||
# workspace-symbol) to conn concurrently with t2's LS responses — a lock
|
||||
# prevents interleaved/corrupted frames on the wire.
|
||||
send_lock = threading.Lock()
|
||||
locked_conn = _LockedConn(conn, send_lock)
|
||||
|
||||
# t1: client → LS (finishes when client closes write side)
|
||||
t1 = threading.Thread(
|
||||
target=_client_to_ls,
|
||||
args=(conn_file, locked_conn, proc.stdin, session),
|
||||
args=(conn_file, proc.stdin, session),
|
||||
daemon=True,
|
||||
)
|
||||
# t2: LS → client (finishes when LS closes stdout)
|
||||
t2 = threading.Thread(
|
||||
target=_ls_to_client,
|
||||
args=(proc.stdout, locked_conn, session),
|
||||
args=(proc.stdout, conn, session),
|
||||
daemon=True,
|
||||
)
|
||||
t1.start()
|
||||
|
||||
11
principals_catalog.json
Normal file
11
principals_catalog.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"params": {
|
||||
"additionalAccess": [
|
||||
{
|
||||
"id": "c88bf29d-b13a-4153-9738-8995085a451e",
|
||||
"label": "LRIADMPRO-IaC-Bicep",
|
||||
"description": "IaC Bicep pipeline service principal"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,6 @@ ilsp = "ilsp.server:main"
|
||||
where = ["."]
|
||||
include = ["ilsp*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"ilsp.bicep_lsp" = ["*.json"]
|
||||
"ilsp.yaml_lsp" = ["*.json"]
|
||||
"ilsp.python_lsp" = ["*.json"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
|
||||
214
scripts/sync_principals_catalog.py
Normal file
214
scripts/sync_principals_catalog.py
Normal 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()
|
||||
@@ -1,132 +0,0 @@
|
||||
import json
|
||||
|
||||
from ilsp.bicep_lsp.local_features import (
|
||||
compute_folding_ranges,
|
||||
compute_signature_help,
|
||||
compute_workspace_symbols,
|
||||
)
|
||||
|
||||
|
||||
# ── signatureHelp ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_signature_help_simple_call():
|
||||
lines = ["var id = resourceId('Microsoft.Storage/storageAccounts', "]
|
||||
result = compute_signature_help(lines, 0, len(lines[0]))
|
||||
assert result is not None
|
||||
sig = result["signatures"][0]
|
||||
assert sig["label"].startswith("resourceId(")
|
||||
assert result["activeParameter"] == 1
|
||||
|
||||
|
||||
def test_signature_help_first_arg():
|
||||
lines = ["var g = guid("]
|
||||
result = compute_signature_help(lines, 0, len(lines[0]))
|
||||
assert result is not None
|
||||
assert result["activeParameter"] == 0
|
||||
assert result["signatures"][0]["label"].startswith("guid(")
|
||||
|
||||
|
||||
def test_signature_help_unknown_function_returns_none():
|
||||
lines = ["var x = notARealFunction("]
|
||||
result = compute_signature_help(lines, 0, len(lines[0]))
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_signature_help_outside_call_returns_none():
|
||||
lines = ["var x = 'hello world'"]
|
||||
result = compute_signature_help(lines, 0, len(lines[0]))
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_signature_help_nested_call_uses_innermost():
|
||||
lines = ["var x = concat(toLower("]
|
||||
result = compute_signature_help(lines, 0, len(lines[0]))
|
||||
assert result is not None
|
||||
assert result["signatures"][0]["label"].startswith("toLower(")
|
||||
|
||||
|
||||
# ── foldingRange ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_folding_range_multiline_object():
|
||||
lines = [
|
||||
"resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {",
|
||||
" name: 'demo'",
|
||||
" sku: {",
|
||||
" name: 'Standard_LRS'",
|
||||
" }",
|
||||
"}",
|
||||
]
|
||||
ranges = compute_folding_ranges(lines)
|
||||
assert {"startLine": 0, "endLine": 5, "kind": "region"} in ranges
|
||||
assert {"startLine": 2, "endLine": 4, "kind": "region"} in ranges
|
||||
|
||||
|
||||
def test_folding_range_ignores_single_line_brackets():
|
||||
lines = ["var x = { a: 1 }"]
|
||||
ranges = compute_folding_ranges(lines)
|
||||
assert ranges == []
|
||||
|
||||
|
||||
def test_folding_range_comment_block():
|
||||
lines = [
|
||||
"// line one",
|
||||
"// line two",
|
||||
"// line three",
|
||||
"var x = 1",
|
||||
]
|
||||
ranges = compute_folding_ranges(lines)
|
||||
assert {"startLine": 0, "endLine": 2, "kind": "comment"} in ranges
|
||||
|
||||
|
||||
def test_folding_range_short_comment_block_not_folded():
|
||||
lines = ["// only one line", "var x = 1"]
|
||||
ranges = compute_folding_ranges(lines)
|
||||
assert not any(r["kind"] == "comment" for r in ranges)
|
||||
|
||||
|
||||
# ── workspace/symbol ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_workspace_symbol_finds_declarations():
|
||||
docs = {
|
||||
"file:///a.bicep": [
|
||||
"param location string = 'westeurope'",
|
||||
"var storageName = 'demo'",
|
||||
"resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {",
|
||||
" name: storageName",
|
||||
"}",
|
||||
"module roleAssignment 'br/modules:modules/roleassignments:2.0.x' = {",
|
||||
" name: 'ra'",
|
||||
"}",
|
||||
],
|
||||
}
|
||||
results = compute_workspace_symbols(docs, "")
|
||||
names = {r["name"] for r in results}
|
||||
assert names == {"location", "storageName", "sa", "roleAssignment"}
|
||||
|
||||
|
||||
def test_workspace_symbol_filters_by_query():
|
||||
docs = {
|
||||
"file:///a.bicep": [
|
||||
"var storageName = 'demo'",
|
||||
"var otherThing = 'x'",
|
||||
],
|
||||
}
|
||||
results = compute_workspace_symbols(docs, "storage")
|
||||
assert len(results) == 1
|
||||
assert results[0]["name"] == "storageName"
|
||||
|
||||
|
||||
def test_workspace_symbol_query_is_case_insensitive():
|
||||
docs = {"file:///a.bicep": ["var StorageName = 'demo'"]}
|
||||
results = compute_workspace_symbols(docs, "storagename")
|
||||
assert len(results) == 1
|
||||
|
||||
|
||||
def test_workspace_symbol_searches_across_multiple_documents():
|
||||
docs = {
|
||||
"file:///a.bicep": ["var fromA = 1"],
|
||||
"file:///b.bicep": ["var fromB = 2"],
|
||||
}
|
||||
results = compute_workspace_symbols(docs, "")
|
||||
names = {r["name"] for r in results}
|
||||
assert names == {"fromA", "fromB"}
|
||||
@@ -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)
|
||||
@@ -502,7 +539,6 @@ def test_session_records_and_pops_context():
|
||||
assert ctx["type"] == "param"
|
||||
assert ctx["module"] == "appservice"
|
||||
|
||||
# Second pop (or any untracked id) returns None — the response must pass
|
||||
# through untouched rather than being mistaken for a completion response.
|
||||
assert session.pop_context(42) is None
|
||||
# Second pop returns unknown
|
||||
assert session.pop_context(42)["type"] == "unknown"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user