Skip to content

Azure IPAM v4.0.0 Release - #377

Open
DCMattyG wants to merge 220 commits into
mainfrom
ipam-3.7.0
Open

Azure IPAM v4.0.0 Release #377
DCMattyG wants to merge 220 commits into
mainfrom
ipam-3.7.0

Conversation

@DCMattyG

@DCMattyG DCMattyG commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Azure IPAM v4.0.0

This is a major release that delivers significant framework upgrades, a data grid migration, authentication modernization, comprehensive documentation overhaul, revamped examples, and numerous bug fixes.


Major Framework & Dependency Upgrades

  • React 18 → 19: Full migration including removal of forwardRef wrappers (ref-as-prop), removal of PropTypes, explicit null for useRef() calls, and migration of LoadingButton to Button
  • MSAL v4 → v5 (@azure/msal-browser 4.x → 5.x, @azure/msal-react 3.x → 5.x): Removed obsolete config, consolidated event types, fixed silent token timeout recovery (timed_out error code), and prevented iframe fallback timeout loops
  • Inovua React Data Grid → AG Grid (ag-grid-community / ag-grid-react 36.x): Complete migration to AG Grid including centralized DataGrid component, custom styling, column state persistence, unified data loading overlays, and custom cell renderers (drill-down, info, progress)
  • Vite 7 → 8 (vite 7.x → 8.x, @vitejs/plugin-react 5.x → 6.x): Migrated to Vite 8 which replaces Rollup with Rolldown and esbuild with Oxc for bundling, transforms, and minification. Removed vite-plugin-eslint2 (redundant with editor-based linting and incompatible with Vite 8)
  • ESLint 10: Upgraded from ESLint 9 to 10 with modern React linting plugins (@eslint-react/eslint-plugin v5, eslint-plugin-react-hooks v7 — which now consolidates the React Compiler lint rules per the React Compiler 1.0 release), replacing eslint-plugin-react. Added dist/ ignore, fixed no-useless-assignment violations, and removed unused eslint-plugin-jest. Resolved all remaining lint warnings as part of the React 19 modernization — useContextuse, <Context.Provider><Context>, ref naming conventions, stable list keys, hoisted static styled components, migration of SnackbarUtils to notistack's standalone enqueueSnackbar, and moved "ref assigned during render" patterns into useEffect
  • MUI v7 → v9 (@mui/material 7.3.x → 9.0.x, @mui/icons-material 7.3.x → 9.0.x): Major two-version jump. Migrated deprecated component props to the unified slots/slotProps API (largely via @mui/codemod), moved deprecated system props into sx, replaced the removed Unstable_Grid2 with the new default Grid (using size={{ xs: N }}), renamed removed Outline (no "d") icon exports to their Outlined counterparts, and removed @mui/lab (no longer needed — LoadingButton's loading prop is now native to Button)
  • React Router 7 → 8 (react-router 7.x → 8.x): Major version upgrade. The UI uses declarative-mode routing (BrowserRouter / Routes / Route) with all imports already sourced from react-router, so no application code changes were required. v8 raises the minimum runtime to Node.js 22.22.0 (and React 19.2.7+)
  • Updated NPM packages across the board (Vite 7.3.x, React Router 7.13.x, MUI 7.3.x, etc.)

Engine & Backend

  • Azure Function Blueprints: Implemented Blueprint-based function naming for improved clarity
  • Python dependency cleanup: Removed msal, azure-common, azure-keyvault-secrets, and six; added azure-mgmt-resource-subscriptions to address Azure SDK module separation
  • Python linting: Added pyproject.toml with Ruff linter configuration (pycodestyle, pyflakes, isort). Resolved all lint violations across 18 engine files — bare except clauses, wildcard imports replaced with explicit names, unused imports/variables, invalid escape sequences, import sorting and grouping
  • Reservation logic hardening: Fixed CIDR overlap detection during auto-fulfillment, added validation for all in-block vNet prefix overlap checks, and made auto-fulfillment idempotent by deduping existing block vNet associations
  • Endpoint fix: Standalone NICs are now included in the Endpoint list (fixes Standalone NICs not listed in Endpoint node #371)
  • Network associations fix: Resolved improper handling of missing vNETs and vHUBs (fixes Error fetching available IP Block networks #350)
  • Next available network fix: nextAvailableVNet aborted its entire Block list search when the first Block could not satisfy the requested size under smallest_cidr, returning a 500 instead of evaluating the remaining Blocks. max() was called on an empty candidate list, and the resulting ValueError escaped the loop. The same missing guard in nextAvailableSubnet and in single-Block Reservation creation replaced their intended error messages with an unhandled exception (fixes nextAvailableVNet fails when using multiple blocks and smallest_cidr option true if no IP range is available in first block #384)
  • HTTP status codes corrected for client and capacity errors: Conditions the API explicitly anticipates were reported as 500, making them indistinguishable from genuine server faults, prompting pointless client and proxy retries, and counting against server-error metrics. Requests that cannot be satisfied by current allocation state now return 409, as does a JSON patch that conflicts with the stored document; a malformed JSON patch now returns 400. Both already matched the codes used by neighboring checks in the same handlers. Errors originating from Azure, Cosmos DB, or the site configuration are unchanged and still return 500
  • Development reload flag removed from production startup: uvicorn was started with --reload in both production init scripts. Under a read-only run-from-package mount its watcher tracked thousands of files that can never change, and its supervisor process stayed alive when the application crashed — so the container kept running while serving nothing, and health checks failed against an apparently healthy process. A startup failure now exits, so the platform restarts the application and the fault is visible. engine/Dockerfile.dev retains the flag, where hot reload is intended
  • TLS validation moved to the OS trust store: The JWKS fetch backing token validation used requests, which verifies against the CA bundle shipped inside certifi rather than the operating system trust store. WEBSITES_INCLUDE_CLOUD_CERTS populates the OS store, so sovereign cloud roots were present but invisible to that code path — secret cloud (IL6) deployments rejected every token with CERTIFICATE_VERIFY_FAILED while passing in every commercial cloud, where certifi covers the endpoints. Every other outbound call already used aiohttp, which reads the OS store; the JWKS fetch was the lone exception and now matches. The metrics heartbeat moved off requests as well, and a Ruff banned-api rule now fails the build on import requests or import httpx, since the defect is invisible to commercial-cloud testing and would otherwise return unnoticed
  • Entra signing keys cached: Token validation fetched the tenant JWKS document and rebuilt the RSA public key from its JWK on every authenticated request, spending a TLS handshake, an HTTP round trip and a key conversion per API call. Keys are now cached in process following Microsoft's signing key rollover guidance — a one hour refresh with jitter, a twenty four hour hard expiry, and a forced refresh on an unknown kid floored at once per five minutes. Concurrent refreshes collapse to a single fetch, and a failed refresh serves the last known good keys rather than failing closed. The floor also closes an amplification vector, where a flood of tokens bearing bogus kid values previously produced one outbound fetch each on behalf of unauthenticated callers

UI & UX Improvements

  • Drill-down navigation in Discover (fixes Navigation Improvements #183): Added hierarchical drill-down with multi-filter pass-through and hidden column auto-reveal when filters are active
  • Centralized authentication handling: New AuthHandler for MSAL error handling, centralized token acquisition via tokenService
  • Custom DraggablePaper component: Replaced react-draggable package with a purpose-built component
  • Associations UX: Shifted API work to Redux thunks, optimized data refresh, fixed infinite update loops by separating initial grid selection from user selection state
  • Planner data loading: Addressed issue where Planner would not load when data was incomplete (fixes Planner will not load #366)
  • Reservation UX: Fixed view reversion after cancelling a Reservation, fixed column sorting, and streamlined the interface
  • Unified grid experience: Centralized DataGrid and ConfigureGrid components with shared filter utilities, consistent loading overlays, and AG Grid custom styling (brightness filter for row hover)
  • Search bar: Fixed messaging when no resources are found in Azure IPAM scope
  • API errors without the standard envelope are now surfaced: The Axios response interceptor read error.response.data.error unconditionally, so any response that did not use the { error } envelope produced an Error with an empty message — and an error snackbar with no text at all. Unhandled engine exceptions return plain text, request validation failures return { detail }, and proxy errors return HTML; each is now resolved to a message, falling back to the Axios status text. This also fixes silent failures on any request rejected by model validation, which had always returned 422

Notifications & Service Management

  • New notification framework (GET /api/notifications, POST /api/notifications/{id}/resolve): A self-describing, API-first advisory system. Server-side detectors emit notifications that the UI — or any API / IaC consumer — can read, while remediation is resolve-by-reference: the client asks the backend to resolve a notification by id and the backend owns all the logic (admin-gated, with an active-notification guard). Adding a new advisory is a single detector module registered in one list
  • Registry-migration detector: Flags deployments still pulling container images from the legacy public registry (azureipam.azurecr.io, critical) or the development registry (azureipamdev.azurecr.io, warning) and offers a one-click remediation that repoints the App Service / Function LinuxFxVersion to registry.azureipam.com. The target image is validated as anonymously pullable before any change is applied, then the app restarts to pull it — complementing the update script's auto-migration with an in-app path
  • Update-available detector: Compares the running IPAM_VERSION against the latest published GitHub release and surfaces an informational notice linking to the update guide. Fails safe (no notification, no error) on network or rate-limit failures
  • Platform-migration detector: Flags deployments still running on the legacy Docker Compose App Service model (DEPLOYMENT_STACK == "LegacyCompose", critical) and links to the migration guide ahead of Microsoft's March 31, 2027 retirement of Docker Compose support for Azure App Service. Link-only guidance (no in-app remediation) since migration is a scripted, multi-step process run from the operator's workstation
  • Notification center (UI): AppBar bell with a severity-colored unread badge, a severity-ordered list (popover on desktop, full-screen sheet on mobile), a detail dialog with server-driven actions and early admin gating, per-item dismiss, mark-all-read, and a manual refresh that resyncs to the server's truth
  • Service restart gate (UI): A full-screen overlay shown while the backend restarts (e.g. after a registry switch). The live SPA stays in memory, polls /api/status, and confirms recovery via a changed service start time before reloading — with calm, time-keyed messaging and a deliberate manual-reload escape hatch if recovery runs long. Background polling is paused while the gate is up

Deployment, Build & Infrastructure

  • Configuration drift detection and remediation in update: The update script now compares an existing deployment against what a fresh deployment would produce today, presents the differences, and converges them on approval. Detection is based on live Azure resource state rather than the version originally deployed, so each difference is evaluated independently and a partially-current deployment only sees what it actually needs. Covers the container registry endpoint, Python runtime version, App Service startup command, health check, baseline app settings, creation of the staging slot, and Function App slot-sticky content-share settings. Existing values and user-added settings are never overwritten, and removal is restricted to settings Azure IPAM owns that no longer apply to the deployment's shape. Production is converged before the staging slot so a newly created slot inherits corrected values, and the two are kept in sync thereafter so a future swap cannot regress production
  • Version-aware update decisions: Version comparisons across all three update paths (public registry image label, private ACR repository build, and native GitHub release) now use semantic versioning rather than string equality, so prerelease suffixes order correctly. Downgrades are no longer silently applied and reported as updates — the update stops and points to -Force. Images built without a version stamp report the 0.0.0 Dockerfile default and are treated as an unknown version rather than a downgrade
  • Updates against stopped or unreachable deployments: The update script now detects application state up front and reports it. Configuration changes, image builds, and ZIP deployments all continue to work, restarts that would achieve nothing are skipped, and a stopped application is reported as remaining stopped. A new -ContainerType (Debian|RHEL) override on update matches the existing migrate switch, so a private ACR deployment can still be rebuilt when its container distro can't be probed. The distro probe is also now bounded by a timeout, so an application whose container fails to start no longer stalls the update indefinitely
  • Guidance instead of failures for anticipated conditions: update and migrate no longer raise exceptions for situations with a known remedy, such as an undetectable container distro. These now print actionable guidance under the relevant phase heading and exit cleanly, matching how legacy Compose deployments and out-of-resource-group registries were already handled. Genuinely unexpected errors now surface their message on screen rather than only in the log
  • DOCKER_REGISTRY_SERVER_URL removed from all deployment templates: This setting is only required for registries authenticating with stored credentials. Azure IPAM pulls anonymously from the public registry or with a managed identity from a private ACR, so it was never needed, and App Service removes it on its own whenever the container registry is reconfigured — which caused the update script to repeatedly report it as configuration drift
  • Azure PowerShell requirements aligned to 11.4.0: The deployment, update, and migration guides now state a single Azure PowerShell rollup version, and each script's granular #Requires module pins match the versions packaged in that rollup. Previously the update guide understated its requirement (Az.Resources 6.16.0 is only available from Az 11.4.0), and deploy.ps1 pinned the Az 10.3.0 module set while its documentation stated Az 11.0.0
  • Fixed Debian container build port: update.ps1 built Debian images with PORT=80 while deploy.ps1, migrate.ps1, and the Dockerfile default all use 8080
  • Public container registry migrated: The publicly hosted Azure IPAM container images have moved from azureipam.azurecr.io to the new registry.azureipam.com endpoint. The deployment and migration Bicep templates now reference the new registry by default, while the update script continues to recognize the legacy azureipam.azurecr.io endpoint so existing deployments keep working. The Docker Compose migration tooling intentionally still targets the legacy endpoint, as it only ever processes pre-existing legacy deployments
  • Migration robustness: The Docker Compose migrate script now halts on a non-standard or unresolvable container registry with guidance to re-run using the -JsonFile override, instead of silently falling back to the public registry. A new -ContainerType (Debian|RHEL) override handles cases where the source app is stopped/unreachable and its distro can't be auto-detected
  • RHEL images updated to UBI9: Including the container-image overrides in the deploy, update, and migrate PowerShell scripts
  • Node.js minimum raised to 22.22.0: Required by React Router v8; enforced in the build.ps1 version gate and the UI package.json engines field
  • Dockerfiles optimized: Improved layering, removed unneeded steps across all container images. Resolved Hadolint lint violations (ADDCOPY, JSON notation for CMD/ENTRYPOINT, pipefail for piped RUN commands). Added centralized .hadolint.yaml for rule suppressions
  • KeyVault Soft Delete added to deployment and migration Bicep templates (fixes Enable soft delete for Key Vault #373)
  • Build flexibility: Added support for building with either current or latest NPM/Python packages
  • CI/CD path exclusions: Added exclusions to avoid unnecessary test/build runs for documentation-only changes
  • GitHub Actions modernized: Bumped all workflow actions to their latest versions (checkout@v6, setup-node@v6, setup-python@v6, github-script@v9, create-github-app-token@v3, azure/login@v3, hadolint-action@v3.3.0) to address the Node.js 20 runner deprecation
  • Azure PowerShell SDK v14 compatibility: Fixed Get-AzAccessToken breaking changes (fixes Breaking changes to Get-AzAccessToken #343); updated deploy & migrate scripts
  • OCI version labels on container images: All production images now carry standard OCI labels (org.opencontainers.image.version, .title, .source), stamped at build time via a new IPAM_VERSION build arg. This lets you determine the exact version behind a floating tag like latest by inspecting the registry — no pull or run required (e.g. docker buildx imagetools inspect or az acr manifest show). Covers all deb, rhel, and func variants across the root, engine, ui, and lb images, with the build workflow passing --build-arg IPAM_VERSION to every az acr build. Labels begin with the v4.0.0 release images
  • Release tag parsing hardened: Version extraction now strips only a leading v (^v) from the release tag, preserving suffixes such as -preview
  • Internet-restricted cloud deployments fixed: The ZIP-deploy path used by internet-restricted clouds (AZURE_US_GOV_SECRET) never resolved its bundled Python packages. init.sh referenced an APP_PATH variable that is defined nowhere in the repository — App Service exposes it only to interactive SSH sessions via ~/.bashrc, which a non-interactive startup command never reads — so PYTHONPATH expanded to a non-existent path at the filesystem root and no bundled dependency could be imported. The application root is now derived from the script's own location, matching what function_app.py already did for the Function App entry point. The accompanying PATH export was removed: it pointed at packages while pip installs console scripts to packages/bin, and nothing invokes them
  • Deployment archive now targets the App Service runtime rather than the build host: pip install --target resolves wheels for the machine running pip. When the GitHub Actions runner moved to Ubuntu 24.04, cryptography began resolving to a manylinux_2_34 wheel that cannot load on the App Service Python 3.11 image (Debian bullseye, glibc 2.31), producing GLIBC_2.33 not found at startup. Wheel resolution is now pinned explicitly (--only-binary=:all:, --platform manylinux2014_x86_64, --implementation cp, --python-version, --abi), with the Python tag and ABI derived from engine/app/version.json. manylinux2014 (glibc 2.17) is targeted deliberately, since sovereign clouds can run older stamps than commercial Azure
  • Native module verification gate: The build now fails if any bundled shared object targets the wrong Python ABI, requires a newer glibc than the platform tag allows, or is a Windows .pyd. Both defects above were invisible at build time and only surfaced after deployment — and the ABI mismatch presented as ModuleNotFoundError, which reads like a missing dependency rather than a build fault
  • Sovereign cloud root certificates now trusted: Secret cloud deployments (AZURE_US_GOV_SECRET, IL6) run against endpoints whose certificate chains are issued by that cloud's own roots, which are not present in the App Service image's default trust store. Every outbound TLS call the engine makes — Key Vault references, Cosmos DB, ARM, Microsoft Graph — therefore failed certificate validation. WEBSITES_INCLUDE_CLOUD_CERTS is now set for that cloud in the deployment, update, and migration templates, so the platform injects the cloud's root certificates. The update script's configuration drift detection also adds it to existing secret cloud deployments. This setting is necessary but was not sufficient on its own — see the TLS validation fix under Engine & Backend, without which the engine still could not validate tokens in that cloud
  • Build script failure reporting: Seven prerequisite checks ended in a bare exit, which returns 0. A build that could not find NodeJS, or that rejected the installed Python version, printed errors and then reported success to CI. All now exit non-zero. The exception handler also printed an unassigned variable in place of the log path
  • Faster archive creation: Compress-Archive took 353 seconds to package the 6,404-file deploy archive; ZipFile.CreateFromDirectory produces an identically sized result in 11 seconds. The new API also preserves Unix file modes, which Compress-Archive flattened to 0644
  • CI Python version single-sourced: All three workflows now read the interpreter version from engine/app/version.json instead of pinning 3.11 by hand. This matters most in the versioning workflow, which regenerates requirements.lock.txt — dependency resolution is Python-version sensitive, so a lock file produced on the wrong interpreter can omit packages the runtime requires

Documentation Overhaul

  • Massively revamped How-To docs: authentication, exclusions, Discover, Reservations, External Networks, and Virtual Network Associations sections
  • New documentation: Comprehensive automation docs, API docs for vNet Associations, initial External Networks docs, detailed Reservations feature docs
  • 50+ screenshots added or replaced to reflect the current UI
  • Fixed all markdown warnings/errors and added .markdownlint.json configuration
  • Doc cleanup: Fixed stale links, grammar, spelling issues, deprecated folder descriptions, and undocumented switches across all sections

Examples

  • Terraform example revamped: Migrated from Shell scripts to the official Azure IPAM Terraform provider
  • Azure ESLZ example modernized: Updated resource API references, modern coding standards, and improved parameterization
  • Script examples reorganized: PowerShell and Shell scripts moved into a dedicated examples/scripts/ folder with new helper scripts and README
  • Token helper function: Standardized access token generation; removed legacy Microsoft Graph SDK v1 support

Testing

  • Added tests for Virtual Network Association permutations
  • Expanded overall testing coverage with numerous additional Pester tests
  • Updated test expectations to align with additionally created resources
  • Added Tools coverage for Block list evaluation — falling through to a later Block when an earlier one cannot satisfy the requested size (with and without smallest_cidr), and the genuinely exhausted case
  • CI lint gate: Added pre-deployment lint job to the testing workflow (ESLint, Vite build verification, Ruff for Python, Bicep template validation, Hadolint for Dockerfiles). Deploy is skipped if any check fails, preventing wasted Azure resources

Bug Fixes

[major]

…that were not being accounted for properly
…ue to improper handling of missing vNETs and vHUBs
DCMattyG added 30 commits July 1, 2026 17:45
…uction-accurate

Improve the update workflow's handling of container deployments and the
auto-provisioned staging slot so updates are safer, idempotent, and consistent.

- Tag built images with both the app version and `latest`, and pass
  IPAM_VERSION as a build arg, matching the CI build convention
- Skip the image build when the running version already matches the
  repository (unless -Force), using the bare version from /api/status
- Mirror production's actual ACR authentication method onto the staging
  slot (AcrUseManagedIdentityCreds) instead of inferring it from the
  registry name, so admin/anonymous and non-ACR registries are no longer
  forced to use a managed identity
- Treat a private registry outside the app's resource group as a clean,
  non-fatal manual-handoff instead of a hard failure
- Correct the staging slot for container function apps: match production's
  kind (functionapp,linux,container) and skip the content-share sticky
  settings that are not permitted as slot settings
- Normalize per-branch console output: consistent manual-handoff sections
  and async-completion guidance across the container and native paths
Bring the update guide in line with the current update.ps1 behavior.

- Note that private ACR container deployments now compare the running
  version and skip the image build when already up to date
- Document version+latest image tagging (ipam:<version> + ipam:latest,
  ipamfunc:<version> + ipamfunc:latest)
- Explain the same-resource-group requirement for automated builds and
  the manual image-update handoff when the registry is elsewhere
- Refresh the Docker Compose detection example to match the new output
- Split the multi-part Additional Requirements and Version Comparison
  items into child bullets for readability
Harden the SPA's MSAL redirect handling so a service restart no longer
triggers a cascading interaction_in_progress loop:

- Poll the public /api/status endpoint without a bearer token so the
  restart gate's recovery poll never triggers MSAL token acquisition.
- Reload the page only when no MSAL interaction is in progress, so an
  in-flight redirect is never interrupted mid-handshake.
- Replace the hand-rolled Login component with MSAL's native
  MsalAuthenticationTemplate for initial sign-in, removing the second
  interactive-redirect owner that raced with AuthHandler.
- Simplify AuthHandler to a single loginRedirect re-auth path and drop
  a dead acquireTokenRedirect branch (ACQUIRE_TOKEN_FAILURE events carry
  a null payload, so it never executed).
MsalAuthenticationTemplate internally calls acquireTokenSilent with the
default (iframe-capable) cache policy and throws on error, which crashed
the UI in privacy browsers that block third-party cookies. Restore the
AuthenticatedTemplate/UnauthenticatedTemplate + Login sign-in, which only
uses loginRedirect (no iframe). Keeps the unauthenticated /api/status poll
and the AuthHandler cleanup.
…d update detectors

Adds a self-describing notification framework (GET /api/notifications,
POST /api/notifications/{id}/resolve) with admin-gated, resolve-by-reference
remediation. Includes detectors for legacy/dev container-registry migration
(with a server-owned "switch to official registry" remediation) and for
available GitHub releases. Replaces the previous service router/models.
…art gate

Adds the AppBar notification center (bell, badge, list, manual refresh),
the detail dialog with server-driven resolve actions, and the full-screen
service restart gate that polls /api/status for recovery. Registers the
notifications/restart reducers and pauses background polling during restarts.
control.py was never staged in the slot-safe convergence commit (fcb3379),
even though app/schema/convergence.py, app/schema/steps.py, and
app/routers/health.py all import from it. Runtime deployments worked only
because they were built from the working tree; a clean checkout would fail
to import app.schema.control on startup. Commit the file to complete the
feature.
Order the app.schema.control import after app.routers.common.helper in
health.py and steps.py. Import ordering only; no functional change.
Adds a Notifications context to the Pester integration suite (ordered to match
the FastAPI router mount order). Covers GET /api/notifications (200 + response
shape validation) and the safe, non-mutating POST /api/notifications/{id}/resolve
guard paths: 404 for an unknown id, 404 for a link-only notification with no
remediation, and 409 for a remediable notification that isn't currently active.
The destructive resolve happy path is intentionally left to engine unit tests.
Mounts the notifications router before health so the route order in FastAPI
(and the generated Swagger docs) reads consistently.
… handling with update

Rework the Docker Compose to single-container migration so it mirrors the proven
registry logic in update.ps1 and shares identical helper functions, in preparation
for extracting a common module.

- Add -ContainerType (Debian|RHEL) to override container-distro auto-detection when
  the source app is stopped/unreachable or is detected incorrectly
- Classify the container registry into three cases (public managed, private managed
  ACR, divergent) and stop with actionable guidance on divergent registries
- Unify logging between migrate.ps1 and update.ps1: Write-LogFile plus transcript,
  detail, and debug log files, native -Debug support, and try/catch/finally
  transcript teardown
- Rename functions to satisfy PSUseSingularNouns (Get-BuildLog, Get-WebAppDetail,
  Get-WebAppStatusDetail)
- Add PSScriptAnalyzerSettings.psd1 and wire it into workspace settings; both scripts
  now pass PSScriptAnalyzer with zero warnings
- Update migration and update docs for -ContainerType, -Debug, and corrected log
  file names (detail_/debug_)
…rate

Bring deploy.ps1 to zero PSSA warnings/errors (matching update.ps1 and
migrate.ps1) and tighten the shared analyzer configuration. All changes are
lint/cosmetic only — no runtime behavior changes.

deploy.ps1:
- Suppress intentional ConvertTo-SecureString token normalization on
  Get-AccessToken (ephemeral access token, not a stored credential)
- Suppress false-positive PSReviewUnusedParameter on EngineAppName, UIAppId
  and EngineAppId (consumed inside Invoke-WithGraphRetry script blocks that
  PSSA cannot trace into — removal would break app/consent creation)
- Rename plural-noun functions to singular: Get-BuildLogs -> Get-BuildLog,
  Deploy-IPAMApplications -> Deploy-IPAMApplication,
  Save-Parameters -> Save-Parameter, Import-Parameters -> Import-Parameter
- Lowercase language keywords (Function/Param/DynamicParam) per MS style

Casing cleanup (deploy, update, migrate):
- Operators -Not -> -not; cmdlet get-date -> Get-Date; -format -> -Format
- Remove redundant trailing ';' line terminators (update, migrate)

PSScriptAnalyzerSettings.psd1:
- Enable PSUseCorrectCasing (commands, keywords, operators)
- Enable PSPlaceOpenBrace (one-true-brace regression guard)
- Enable PSUseCompatibleSyntax (TargetVersions 7.2)
- Enable PSAvoidSemicolonsAsLineTerminators

Verified: all three scripts parse clean and report 0 warnings/errors and
0 information findings under the repo settings.
…l descriptions

Fill in the placeholder "DOCSTRING" docstrings that were previously added
to satisfy the linter with actual descriptions of each symbol's purpose.

- models.py: describe all response, request, azure, admin, user, tool,
  health, and status Pydantic models plus the IPv4 type helpers
- routers/azure.py: describe the subscription, vWAN, and VMSS SDK helpers
- routers/common/helper.py: describe the JWT, credential, Cosmos DB, and
  Azure Resource Graph helpers
- routers/internal.py: describe the multi_helper concurrency helper
…tion

Add a platform-migration detector that emits a critical, non-dismissible
notification when DEPLOYMENT_STACK == "LegacyCompose", ahead of Microsoft's
March 31, 2027 retirement of Docker Compose support for Azure App Service.

Link-only guidance (no server-owned resolve) pointing at the migration guide,
mirroring the update detector: migration is a scripted, multi-step process run
from the operator's workstation. Registered in the detector list and documented
in the v4.0.0 release notes.
…tput

Skip unnecessary work by checking whether a deployment is already current
before building, restarting, or running prerequisite checks.

- Private ACR: compare running vs. repo version first and short-circuit when
  up to date, before the ACR lookup and CLI/PowerShell context checks
- Public ACR: add Get-RegistryImageVersion to read the OCI version label via
  the Registry v2 API; skip the restart when current, fall back to
  restart-to-update when the label is missing (pre-OCI) or unreadable
- Public ACR: drop the redundant explicit restart after a registry repoint,
  since updating LinuxFxVersion already recycles the App Service
- -Force still deploys latest regardless
- migrate: handle a non-Compose deployment gracefully (guide to update.ps1)
  instead of throwing
- Align update/migrate output: inline "Success"/"Failed" status, per-attempt
  restart lines, and a dedicated "Fetching build logs" step

Affects: update/update.ps1, migrate/migrate.ps1
Pin azure-mgmt-resource-subscriptions to 1.0.0 in requirements.lock.txt
since the previously pinned 1.1.0 does not exist on PyPI and broke
dependency resolution.
Shorten the notification title from "Action required: Azure IPAM
container registry has moved" to "Azure IPAM container registry has
moved" for cleaner wording.
The docs state a rollup version while the scripts pin individual modules for
load-time performance, but the two had drifted apart:

- docs/update stated Az 10.3.0 while update.ps1 requires Az.Resources 6.16.0,
  which first ships in Az 11.4.0, so following the prerequisites exactly would
  fail the #Requires check.
- deploy.ps1 pinned the module set from Az 10.3.0 while its documentation
  stated Az 11.0.0.

Align every Az module pin with the versions packaged in Az 11.4.0 and state
that rollup version consistently across the deployment, update and migration
guides. migrate.ps1 already matched. The Microsoft.Graph modules are not part
of the Az rollup and are unchanged.
$APP_PATH was referenced in init.sh but never defined anywhere in the
repo. App Service does not export it to the startup command's
environment; Oryx only appends it to ~/.bashrc, which a non-interactive
shell never sources. PYTHONPATH therefore expanded to ":/packages", a
non-existent path at the filesystem root, leaving every bundled
dependency unimportable.

Derive the application root from the script's own location instead,
mirroring what engine/function_app.py already does for the Function App
entry point.

Also:

- Use ${PYTHONPATH:+...} so an unset PYTHONPATH no longer yields a
  leading colon, which silently placed the working directory on the
  import path.
- Drop the PATH export. It pointed at "packages" while pip installs
  console scripts to "packages/bin", those files ship non-executable
  (0644), and nothing invokes them since the app starts via
  "python -m uvicorn".

Only reachable when WEBSITE_RUN_FROM_PACKAGE is set, which is limited to
internet-restricted clouds (AZURE_US_GOV_SECRET). Every changed line is
inside that guard, so the Debian and RHEL single-container images are
unaffected.
pip install --target resolves wheels for the machine and interpreter
running pip, not for the deployment target. When the GitHub Actions
runner moved to Ubuntu 24.04, cryptography began resolving to a
manylinux_2_34 wheel, which cannot load on the App Service Python 3.11
image (Debian bullseye, glibc 2.31), and deployments failed with
"GLIBC_2.33 not found".

Passing --platform alone is not sufficient. pip still fills in the
Python version from the running interpreter, producing cpython-312
extension modules that Python 3.11 silently ignores, which surfaces as
an unrelated-looking ModuleNotFoundError.

Pin all four resolution inputs, deriving the Python tag and ABI from
engine/app/version.json so a runtime bump carries through without
further edits:

  --only-binary=:all: --platform manylinux2014_x86_64
  --implementation cp --python-version <ver> --abi cp<ver>

manylinux2014 (glibc 2.17) is chosen for maximum compatibility, since
sovereign clouds can run older stamps than commercial Azure. The
widening path is documented inline should a dependency ever drop those
wheels.

Add a post-install verification gate that fails the build when a native
module targets the wrong Python ABI, requires a newer glibc than the
platform tag allows, or is a Windows .pyd. Both defects above were
invisible at build time and only surfaced after deployment.

Container images are unaffected; they pip install inside the target
image, so their wheels match by construction.
Seven prerequisite checks ended in a bare `exit`, which returns 0. A
build that could not find NodeJS, or that rejected the installed Python
version, printed red errors and then reported success. In CI the step
went green, and the failure only surfaced later — if at all — when the
release upload found no artifact.

The exception handler also printed `$buildLog`, a variable that is never
assigned, so the "Build Log:" line showed an empty path at precisely the
moment it was needed. Point it at $transcriptLog.

Remove the unreachable condition guarding ZIP creation. $npmBuildErr and
$pipInstallErr are never assigned anywhere in the script, so the test was
always true and its else branch was dead code that also used a bare
`exit`. Control flow is unchanged: failures are caught by the
$LASTEXITCODE checks around npm and pip and by the native module
verification gate, all of which throw into the outer handler.

Also bring the script to zero PSScriptAnalyzer findings, matching
deploy.ps1, update.ps1 and migrate.ps1:

- Drop ValueFromPipelineByPropertyName from all three parameters. The
  script has no process block and is never pipeline-fed, so the binding
  was vestigial. Same fix previously applied to the other scripts.
- Correct Get-Date and -Format casing.
- Suppress PSAvoidUsingPositionalParameters at script scope; npm is an
  external executable, not a cmdlet, so its arguments are not
  PowerShell positional parameters.

Removing the if wrapper re-indents the archive assembly block. Review
with `git diff -w` to see the functional change in isolation.
All three workflows pinned python-version: '3.11' by hand, duplicating a
value that engine/app/version.json already owns. Nothing kept the two in
sync, so a runtime bump would silently leave CI on the previous
interpreter.

This matters most in azure-ipam-version.yml, which regenerates
requirements.lock.txt. Dependency resolution is Python-version sensitive,
so a lock file produced on the wrong interpreter can omit packages that
the target runtime requires, yielding an archive that fails only at
deploy time.

Read the value with jq after checkout and feed it to setup-python.
Reordering was required to make the file readable:

- azure-ipam-assets.yml: checkout promoted to the first step
- azure-ipam-version.yml: setup-python moved below checkout. Checkout
  cannot move earlier because it consumes the GitHub App token generated
  two steps prior.
- azure-ipam-testing.yml: already ordered correctly; read step only

No workflow changes what it does; only which interpreter is installed
and where two steps sit in the sequence. Container base images and the
python:3.11-slim serve images remain pinned separately and are unchanged.
uvicorn was started with --reload in both production init scripts. The
flag is documented as a development aid, and it caused two concrete
problems.

It masked crashes. Under --reload, uvicorn runs a supervisor plus a
child. When the child died during startup the supervisor stayed alive,
so the container kept running and both Docker and App Service saw a
healthy process serving nothing. Removing the flag makes the process
exit on failure, so the platform restarts it and the fault is visible.

It also polled the filesystem continuously. Under
WEBSITE_RUN_FROM_PACKAGE the watched directory is the read-only ZIP
mount, so the watcher tracked thousands of files that can never change.
Termination was slow as well: a signalled shutdown took roughly two
minutes with the reloader versus about one second without it, which
would surface as sluggish restarts and slot swaps.

Verified against the published container image and against a
run-from-package layout: no reloader process is started, and a startup
failure now exits non-zero instead of leaving the container up.

engine/Dockerfile.dev keeps --reload; hot reload is intended there.

Worker count is deliberately left at the default. The reservation
scheduler runs in-process, so additional workers would schedule it more
than once.
Compress-Archive took 353 seconds to package the 6,404-file deploy
archive; ZipFile.CreateFromDirectory produces a byte-identical 49.6 MB
result in 11 seconds. The cost grows with the dependency tree, so it is
paid on every release build.

Resolve the destination to an absolute path before handing it to .NET.
PowerShell cmdlets resolve relative paths against the session location,
whereas System.IO resolves against the process working directory, and
the two are not kept in sync. The release workflow invokes the script
from tools with -Path ../assets, so the archive path resolved outside
the repository entirely and the call failed.

Use -LiteralPath on both Convert-Path calls. With -Path, a directory
containing square brackets is treated as a wildcard character class,
matches nothing, and returns empty without raising an error, which would
silently produce a malformed destination.

The new API also preserves Unix file modes, where Compress-Archive
flattened everything to 0644. Console scripts under packages/bin have
been shipping non-executable as a result; they are now packaged with
their original permissions. Nothing in the deployment depended on the
old behaviour, since shared objects need read rather than execute
access and init.sh is invoked as an argument to bash.

Compress-Archive emitted nine additional directory records. Each has
descendant files, so every directory is still created on extraction and
archive contents are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants