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/
|
COPY scripts/download_bicep_ls.sh /scripts/
|
||||||
RUN chmod +x /scripts/download_bicep_ls.sh && BICEP_VERSION=${BICEP_VERSION} /scripts/download_bicep_ls.sh
|
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 ───────────────────────────────────────────────
|
# ── Stage 2: Python wheel build ───────────────────────────────────────────────
|
||||||
FROM python:3.12-slim AS builder
|
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 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 /opt/bicep-langserver /opt/bicep-langserver
|
||||||
|
COPY --from=bicep-downloader /azdo-pipeline-schema.json /azdo-pipeline-schema.json
|
||||||
|
|
||||||
# Install Python package and dependencies
|
# Install Python package and dependencies
|
||||||
COPY --from=builder /dist/*.whl /tmp/
|
COPY --from=builder /dist/*.whl /tmp/
|
||||||
|
|||||||
@@ -14,49 +14,17 @@ supports LSP WebSocket transport.
|
|||||||
|
|
||||||
## IntelliJ IDEA setup (LSP4IJ)
|
## IntelliJ IDEA setup (LSP4IJ)
|
||||||
|
|
||||||
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then add language servers:
|
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then:
|
||||||
|
|
||||||
### Bicep
|
|
||||||
|
|
||||||
1. **Settings → Languages & Frameworks → LSP → Language Servers → +**
|
1. **Settings → Languages & Frameworks → LSP → Language Servers → +**
|
||||||
- Name: `iLSP Bicep`
|
- Name: `iLSP Bicep`
|
||||||
- Server type: `Command` (not WebSocket — see note below)
|
- Server type: `WebSocket`
|
||||||
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/bicep`
|
- URL: `wss://ilsp.i80.dk/bicep`
|
||||||
- Arguments: (leave blank)
|
|
||||||
- File pattern: `*.bicep;*.bicepparam`
|
|
||||||
|
|
||||||
### 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 → +**
|
3. **Disable Azure Toolkit Bicep completions** — this is critical if you have the
|
||||||
- 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
|
|
||||||
Azure Toolkit plugin installed. Without this step, IDEA merges completions from
|
Azure Toolkit plugin installed. Without this step, IDEA merges completions from
|
||||||
both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`,
|
both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`,
|
||||||
`projectName`, Bicep schema types, etc.) appearing alongside iLSP results.
|
`projectName`, Bicep schema types, etc.) appearing alongside iLSP results.
|
||||||
@@ -75,20 +43,13 @@ tail -f /tmp/lsp_bridge_debug.log
|
|||||||
|
|
||||||
## Neovim setup
|
## 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
|
```lua
|
||||||
-- In your LSP config (e.g. ~/.config/nvim/lua/lsp.lua)
|
-- In your LSP config (e.g. ~/.config/nvim/lua/lsp.lua)
|
||||||
local BRIDGE = "/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh"
|
|
||||||
|
|
||||||
-- Bicep
|
-- Bicep
|
||||||
vim.lsp.start({
|
vim.lsp.start({
|
||||||
name = "ilsp-bicep",
|
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]),
|
root_dir = vim.fs.dirname(vim.fs.find({ "bicepconfig.json", ".git" }, { upward = true })[1]),
|
||||||
filetypes = { "bicep" },
|
filetypes = { "bicep" },
|
||||||
})
|
})
|
||||||
@@ -96,7 +57,7 @@ vim.lsp.start({
|
|||||||
-- YAML (pipeline files)
|
-- YAML (pipeline files)
|
||||||
vim.lsp.start({
|
vim.lsp.start({
|
||||||
name = "ilsp-yaml",
|
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]),
|
root_dir = vim.fs.dirname(vim.fs.find({ ".git" }, { upward = true })[1]),
|
||||||
filetypes = { "yaml" },
|
filetypes = { "yaml" },
|
||||||
})
|
})
|
||||||
@@ -104,7 +65,7 @@ vim.lsp.start({
|
|||||||
-- Python
|
-- Python
|
||||||
vim.lsp.start({
|
vim.lsp.start({
|
||||||
name = "ilsp-python",
|
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]),
|
root_dir = vim.fs.dirname(vim.fs.find({ "pyproject.toml", ".git" }, { upward = true })[1]),
|
||||||
filetypes = { "python" },
|
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_iac_module_sources.py # → iac_source_catalog.json (from DevOpsMCP)
|
||||||
python3 scripts/sync_bicep_modules.py # → bicep_modules_catalog.json
|
python3 scripts/sync_bicep_modules.py # → bicep_modules_catalog.json
|
||||||
|
|
||||||
# 2. Commit + push — Gitea Actions builds the image and Nomad redeploys it
|
# 2. Push and deploy
|
||||||
git add bicep_modules_catalog.json iac_source_catalog.json pipeline_templates_catalog.json
|
bash scripts/push_catalogs.sh
|
||||||
git commit -m "chore: refresh catalogs"
|
|
||||||
git push
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note**: `scripts/push_catalogs.sh` and the `/reload` endpoint only take effect if
|
The `/reload` endpoint on the running service reloads catalogs from volume-mounted
|
||||||
> `/data` is volume-mounted into the running container. **Production (`ilsp.nomad`)
|
files if present, so you can update without a full redeploy for pipeline templates.
|
||||||
> 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`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
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.
|
→ 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
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -95,14 +31,9 @@ make health # curl health endpoint
|
|||||||
|
|
||||||
## Updating catalogs
|
## 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
|
```bash
|
||||||
python3 scripts/sync_pipeline_templates.py # scan AzDO + GHA template repos
|
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
|
bash scripts/push_catalogs.sh # scp to server + /reload
|
||||||
git add *_catalog.json && git commit -m "chore: refresh catalogs" && git push
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploy
|
## 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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Known Azure enum values not always captured in the catalog schema
|
# 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]] = {
|
_KNOWN_ENUMS: dict[str, list[str]] = {
|
||||||
"principalType": ["User", "Group", "ServicePrincipal", "Device", "ForeignGroup"],
|
"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
|
# 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
|
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]]:
|
def _load_iac_source_catalog() -> dict[str, dict[str, Any]]:
|
||||||
"""Load IAC source catalog for enriched param descriptions.
|
"""Load IAC source catalog for enriched param descriptions.
|
||||||
@@ -106,12 +122,14 @@ class BicepModuleCatalog:
|
|||||||
|
|
||||||
_modules: list[dict[str, Any]] = []
|
_modules: list[dict[str, Any]] = []
|
||||||
_iac: dict[str, dict[str, Any]] = {} # module name → IAC source info
|
_iac: dict[str, dict[str, Any]] = {} # module name → IAC source info
|
||||||
|
_principals: dict[str, list[dict[str, Any]]] = {} # param name → [{id, label, description}]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls) -> None:
|
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._modules = _load_catalog()
|
||||||
cls._iac = _load_iac_source_catalog()
|
cls._iac = _load_iac_source_catalog()
|
||||||
|
cls._principals = _load_principals_catalog()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]:
|
def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]:
|
||||||
@@ -258,6 +276,44 @@ class BicepModuleCatalog:
|
|||||||
})
|
})
|
||||||
return items
|
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
|
@classmethod
|
||||||
def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]:
|
def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]:
|
||||||
"""Param completions for a specific module+version combination."""
|
"""Param completions for a specific module+version combination."""
|
||||||
@@ -280,56 +336,6 @@ class BicepModuleCatalog:
|
|||||||
iac_params = cls._iac_param_map(module_name)
|
iac_params = cls._iac_param_map(module_name)
|
||||||
|
|
||||||
items = []
|
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():
|
for param_name, param_info in ver_params.items():
|
||||||
ptype = param_info.get("type", "any")
|
ptype = param_info.get("type", "any")
|
||||||
allowed = param_info.get("allowed", [])
|
allowed = param_info.get("allowed", [])
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .local_features import (
|
|
||||||
compute_folding_ranges,
|
|
||||||
compute_signature_help,
|
|
||||||
compute_workspace_symbols,
|
|
||||||
)
|
|
||||||
from .modules import BicepModuleCatalog
|
from .modules import BicepModuleCatalog
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -135,6 +130,19 @@ class _ProxySession:
|
|||||||
mod_name = last_mod.group(1)
|
mod_name = last_mod.group(1)
|
||||||
mod_ver = last_mod.group(2)
|
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
|
# Check if cursor is after 'paramname: ' on the current line
|
||||||
# (value context — inject enum/allowed values)
|
# (value context — inject enum/allowed values)
|
||||||
value_m = re.search(r"^\s*(\w+):\s*('?)([^'{}]*)$", current)
|
value_m = re.search(r"^\s*(\w+):\s*('?)([^'{}]*)$", current)
|
||||||
@@ -147,43 +155,6 @@ class _ProxySession:
|
|||||||
"has_open_quote": bool(value_m.group(2)),
|
"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 {
|
return {
|
||||||
"type": "param",
|
"type": "param",
|
||||||
"module": mod_name,
|
"module": mod_name,
|
||||||
@@ -192,12 +163,8 @@ class _ProxySession:
|
|||||||
|
|
||||||
return {"type": "unknown"}
|
return {"type": "unknown"}
|
||||||
|
|
||||||
def pop_context(self, msg_id) -> dict | None:
|
def pop_context(self, msg_id) -> dict:
|
||||||
"""Return the tracked completion context for msg_id, or None if this
|
return self.pending.pop(msg_id, {"type": "unknown"})
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# ── Completion injection ───────────────────────────────────────────────────────
|
# ── Completion injection ───────────────────────────────────────────────────────
|
||||||
@@ -243,11 +210,18 @@ def _inject_completions(msg: dict[str, Any], context: dict | None = None) -> byt
|
|||||||
context["param"],
|
context["param"],
|
||||||
context.get("has_open_quote", False),
|
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:
|
else:
|
||||||
# Default: module name completions
|
# Default: module name completions
|
||||||
lru_items = BicepModuleCatalog.as_completion_items()
|
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
|
# Always replace LS completions for private-registry contexts — the
|
||||||
# Bicep LS doesn't know about our ACR, so anything it returns is noise.
|
# 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.
|
# 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()
|
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(
|
def _client_to_ls(
|
||||||
conn_file,
|
conn_file,
|
||||||
conn: socket.socket,
|
|
||||||
proc_stdin,
|
proc_stdin,
|
||||||
session: _ProxySession,
|
session: _ProxySession,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -299,7 +255,6 @@ def _client_to_ls(
|
|||||||
logger.debug("Client→LS: %d bytes", len(body))
|
logger.debug("Client→LS: %d bytes", len(body))
|
||||||
|
|
||||||
# Track document state and completion context (never block forwarding)
|
# Track document state and completion context (never block forwarding)
|
||||||
forward = True
|
|
||||||
try:
|
try:
|
||||||
msg = json.loads(body)
|
msg = json.loads(body)
|
||||||
method = msg.get("method", "")
|
method = msg.get("method", "")
|
||||||
@@ -315,33 +270,9 @@ def _client_to_ls(
|
|||||||
session.update_doc(uri, changes[-1].get("text", ""))
|
session.update_doc(uri, changes[-1].get("text", ""))
|
||||||
elif method == "textDocument/completion":
|
elif method == "textDocument/completion":
|
||||||
session.record_completion_request(msg)
|
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:
|
except Exception:
|
||||||
forward = True # parsing/handling errors must never block forwarding
|
pass # parsing errors must never block forwarding
|
||||||
|
|
||||||
if forward:
|
|
||||||
framed = _frame(body)
|
framed = _frame(body)
|
||||||
proc_stdin.write(framed)
|
proc_stdin.write(framed)
|
||||||
proc_stdin.flush()
|
proc_stdin.flush()
|
||||||
@@ -368,26 +299,10 @@ def _ls_to_client(
|
|||||||
logger.debug("LS→Client: %d bytes", len(body))
|
logger.debug("LS→Client: %d bytes", len(body))
|
||||||
try:
|
try:
|
||||||
msg = json.loads(body)
|
msg = json.loads(body)
|
||||||
context = None
|
context: dict = {}
|
||||||
if "id" in msg and "result" in msg:
|
if "id" in msg and "result" in msg:
|
||||||
context = session.pop_context(msg["id"])
|
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)
|
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:
|
except json.JSONDecodeError:
|
||||||
out = body
|
out = body
|
||||||
conn.sendall(_frame(out))
|
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
|
# Unbuffered read from the socket — critical for correct LSP framing
|
||||||
conn_file = conn.makefile("rb", buffering=0)
|
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: client → LS (finishes when client closes write side)
|
||||||
t1 = threading.Thread(
|
t1 = threading.Thread(
|
||||||
target=_client_to_ls,
|
target=_client_to_ls,
|
||||||
args=(conn_file, locked_conn, proc.stdin, session),
|
args=(conn_file, proc.stdin, session),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
# t2: LS → client (finishes when LS closes stdout)
|
# t2: LS → client (finishes when LS closes stdout)
|
||||||
t2 = threading.Thread(
|
t2 = threading.Thread(
|
||||||
target=_ls_to_client,
|
target=_ls_to_client,
|
||||||
args=(proc.stdout, locked_conn, session),
|
args=(proc.stdout, conn, session),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
t1.start()
|
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 = ["."]
|
where = ["."]
|
||||||
include = ["ilsp*"]
|
include = ["ilsp*"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
|
||||||
"ilsp.bicep_lsp" = ["*.json"]
|
|
||||||
"ilsp.yaml_lsp" = ["*.json"]
|
|
||||||
"ilsp.python_lsp" = ["*.json"]
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.0",
|
"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 asyncio
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
@@ -72,9 +71,8 @@ async def main(uri: str) -> None:
|
|||||||
if msg is None:
|
if msg is None:
|
||||||
break
|
break
|
||||||
await ws.send(msg)
|
await ws.send(msg)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"stdin_to_ws error: {e}", file=sys.stderr)
|
pass
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
|
|
||||||
async def ws_to_stdout() -> None:
|
async def ws_to_stdout() -> None:
|
||||||
try:
|
try:
|
||||||
@@ -82,9 +80,8 @@ async def main(uri: str) -> None:
|
|||||||
data = frame if isinstance(frame, bytes) else frame.encode()
|
data = frame if isinstance(frame, bytes) else frame.encode()
|
||||||
sys.stdout.buffer.write(data)
|
sys.stdout.buffer.write(data)
|
||||||
sys.stdout.buffer.flush()
|
sys.stdout.buffer.flush()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"ws_to_stdout error: {e}", file=sys.stderr)
|
pass
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
|
|
||||||
await asyncio.gather(stdin_to_ws(), ws_to_stdout())
|
await asyncio.gather(stdin_to_ws(), ws_to_stdout())
|
||||||
|
|
||||||
@@ -93,9 +90,4 @@ if __name__ == "__main__":
|
|||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
|
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
try:
|
|
||||||
asyncio.run(main(sys.argv[1]))
|
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"
|
IAC_CATALOG="$DEVOPS_MCP_REPO/iac_source_catalog.json"
|
||||||
ILSP_REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
ILSP_REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
TMPL_CATALOG="$ILSP_REPO/pipeline_templates_catalog.json"
|
TMPL_CATALOG="$ILSP_REPO/pipeline_templates_catalog.json"
|
||||||
|
PRINCIPALS_CATALOG="$ILSP_REPO/principals_catalog.json"
|
||||||
|
|
||||||
echo "── iLSP catalog push ──────────────────────────────"
|
echo "── iLSP catalog push ──────────────────────────────"
|
||||||
|
|
||||||
@@ -50,6 +51,13 @@ if [[ ! -f "$TMPL_CATALOG" ]]; then
|
|||||||
fi
|
fi
|
||||||
echo " ✓ $(basename "$TMPL_CATALOG") ($(du -sh "$TMPL_CATALOG" | cut -f1))"
|
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
|
# Copy iac_source_catalog.json to iLSP repo root so it gets baked into the Docker image
|
||||||
echo ""
|
echo ""
|
||||||
echo " → Copying iac_source_catalog.json to iLSP repo root (for Docker bake) …"
|
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 ""
|
||||||
echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …"
|
echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …"
|
||||||
ssh "$AUTOBOX" "mkdir -p $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"
|
echo " ✓ Upload done"
|
||||||
|
|
||||||
if [[ "$NO_RELOAD" == "true" ]]; then
|
if [[ "$NO_RELOAD" == "true" ]]; then
|
||||||
@@ -78,5 +86,5 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
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."
|
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)
|
@pytest.fixture(autouse=True)
|
||||||
def reset_modules():
|
def reset_modules():
|
||||||
BicepModuleCatalog._modules = []
|
BicepModuleCatalog._modules = []
|
||||||
|
BicepModuleCatalog._principals = {}
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@@ -173,12 +174,8 @@ def test_param_completions_injected_on_param_context():
|
|||||||
assert "roleDefinitionIds" in labels
|
assert "roleDefinitionIds" in labels
|
||||||
assert "principalId" in labels
|
assert "principalId" in labels
|
||||||
assert "principalType" in labels
|
assert "principalType" in labels
|
||||||
# First item is now the snippet completion
|
assert items[0]["sortText"].startswith("0_lru_param_")
|
||||||
assert items[0]["label"] == "⚡ Fill params block"
|
assert items[0]["kind"] == 5 # Field
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def test_param_completion_items_have_insert_text():
|
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
|
"roleassignments", versions=["1.1.x"], schema=_ROLEASSIGNMENTS_SCHEMA
|
||||||
)]
|
)]
|
||||||
items = BicepModuleCatalog.param_completion_items("roleassignments", "1.1.x")
|
items = BicepModuleCatalog.param_completion_items("roleassignments", "1.1.x")
|
||||||
# First item should be the snippet completion
|
for item in items:
|
||||||
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:]:
|
|
||||||
assert item["insertText"].endswith(": ")
|
assert item["insertText"].endswith(": ")
|
||||||
|
|
||||||
|
|
||||||
@@ -311,48 +303,6 @@ def test_detect_param_value_context_open_quote():
|
|||||||
assert ctx["has_open_quote"] is True
|
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():
|
def test_param_value_items_from_catalog_allowed():
|
||||||
"""environmentType completions come from catalog 'allowed' field."""
|
"""environmentType completions come from catalog 'allowed' field."""
|
||||||
BicepModuleCatalog._modules = [_make_module(
|
BicepModuleCatalog._modules = [_make_module(
|
||||||
@@ -404,24 +354,6 @@ def test_param_value_items_known_enum_fallback():
|
|||||||
assert "User" in labels
|
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():
|
def test_param_value_items_empty_for_free_string():
|
||||||
"""A plain string param with no allowed values returns no completions."""
|
"""A plain string param with no allowed values returns no completions."""
|
||||||
BicepModuleCatalog._modules = [_make_module(
|
BicepModuleCatalog._modules = [_make_module(
|
||||||
@@ -455,6 +387,111 @@ def test_param_value_injected_in_completion_response():
|
|||||||
assert labels == ["DEV", "TEST", "PROD"]
|
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():
|
def test_detect_unknown_context_outside_module():
|
||||||
lines = ["var x = 'hello'"]
|
lines = ["var x = 'hello'"]
|
||||||
session = _make_session_with_doc(URI, lines)
|
session = _make_session_with_doc(URI, lines)
|
||||||
@@ -502,7 +539,6 @@ def test_session_records_and_pops_context():
|
|||||||
assert ctx["type"] == "param"
|
assert ctx["type"] == "param"
|
||||||
assert ctx["module"] == "appservice"
|
assert ctx["module"] == "appservice"
|
||||||
|
|
||||||
# Second pop (or any untracked id) returns None — the response must pass
|
# Second pop returns unknown
|
||||||
# through untouched rather than being mistaken for a completion response.
|
assert session.pop_context(42)["type"] == "unknown"
|
||||||
assert session.pop_context(42) is None
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user