325 lines
21 KiB
Markdown
325 lines
21 KiB
Markdown
# Architecture Research
|
|
|
|
**Domain:** Single-container deployment package generator webapp (Intune + NinjaRMM)
|
|
**Researched:** 2026-04-10
|
|
**Confidence:** HIGH (core patterns well-established; .intunewin internals MEDIUM — encryption layer may require IntuneWinAppUtil.exe rather than reimplementation)
|
|
|
|
## Standard Architecture
|
|
|
|
### System Overview
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ Browser (SPA or SSR) │
|
|
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
|
|
│ │ Printer │ │ Driver Library │ │ Package Export │ │
|
|
│ │ Config Form │ │ Browser │ │ (Download Trigger)│ │
|
|
│ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │
|
|
└──────────┼───────────────────┼──────────────────────┼────────────┘
|
|
│ HTTP/REST │ HTTP/REST │ HTTP/REST
|
|
┌──────────┼───────────────────┼──────────────────────┼────────────┐
|
|
│ ▼ ▼ ▼ │
|
|
│ API Layer (HTTP server) │
|
|
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
|
|
│ │ /printers │ │ /drivers │ │ /packages │ │
|
|
│ │ CRUD routes │ │ upload routes │ │ generate routes │ │
|
|
│ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │
|
|
│ │ │ │ │
|
|
├──────────┼───────────────────┼──────────────────────┼────────────┤
|
|
│ ▼ ▼ ▼ │
|
|
│ Service / Domain Layer │
|
|
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
|
|
│ │ Printer │ │ Driver │ │ Package Builder │ │
|
|
│ │ Service │ │ Service │ │ Service │ │
|
|
│ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │
|
|
│ │ │ │ │
|
|
│ │ ┌────────┘ ┌────────┘ │
|
|
│ │ ▼ ▼ │
|
|
│ │ ┌─────────────────┐ ┌─────────────────────────────┐ │
|
|
│ │ │ Driver Store │ │ Package Generators │ │
|
|
│ │ │ (volume mount) │ │ ┌─────────┐ ┌───────────┐ │ │
|
|
│ │ └─────────────────┘ │ │ PS Tmpl │ │ intunewin │ │ │
|
|
│ │ │ │ Engine │ │ Builder │ │ │
|
|
│ │ │ └─────────┘ └───────────┘ │ │
|
|
│ │ │ ┌─────────┐ │ │
|
|
│ │ │ │ ZIP │ │ │
|
|
│ │ │ │ Builder │ │ │
|
|
│ │ │ └─────────┘ │ │
|
|
│ │ └─────────────────────────────┘ │
|
|
│ ▼ │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ Data Layer │ │
|
|
│ │ ┌────────────────┐ ┌─────────────────┐ │ │
|
|
│ │ │ SQLite DB │ │ Filesystem │ │ │
|
|
│ │ │ (printers, │ │ (drivers/ vol, │ │ │
|
|
│ │ │ clients, │ │ tmp/ for │ │ │
|
|
│ │ │ driver refs) │ │ in-progress │ │ │
|
|
│ │ │ │ │ packages) │ │ │
|
|
│ │ └────────────────┘ └─────────────────┘ │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
└───────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Component Responsibilities
|
|
|
|
| Component | Responsibility | Typical Implementation |
|
|
|-----------|----------------|------------------------|
|
|
| API Layer | HTTP routing, request validation, response formatting | FastAPI (Python) or Hono/Express (Node) |
|
|
| Printer Service | CRUD for printer configs, parameter validation | Plain service class over SQLite |
|
|
| Driver Service | File upload handling, driver metadata storage, deduplication | Service + volume-mounted filesystem |
|
|
| Package Builder Service | Orchestrates generation: invokes PS template engine, intunewin builder, ZIP builder | Coordinator service, calls sub-generators |
|
|
| PowerShell Template Engine | Renders printer-specific PS install script from a template | Jinja2 or string template with parameter substitution |
|
|
| IntuneWin Builder | Produces a valid .intunewin file from a source folder | Wraps bundled IntuneWinAppUtil.exe via subprocess call |
|
|
| ZIP Builder | Produces NinjaRMM-ready ZIP: PS script + driver files | Python zipfile / Node archiver in-process |
|
|
| SQLite DB | Stores printer configs, client groups, driver metadata references | SQLite via a lightweight ORM or raw queries |
|
|
| Driver Store (volume) | Holds uploaded driver ZIP/INF blobs, persists across container restarts | Docker volume, addressed by content-hash filenames |
|
|
| Temp workspace | Staging area for a build: assembled files before zipping | tmpfs or host-path temp dir, cleaned after download |
|
|
|
|
## Recommended Project Structure
|
|
|
|
```
|
|
imptune/
|
|
├── api/ # HTTP route handlers
|
|
│ ├── printers.py # /printers CRUD
|
|
│ ├── drivers.py # /drivers upload + list
|
|
│ ├── packages.py # /packages/generate endpoints
|
|
│ └── clients.py # /clients group management
|
|
├── services/
|
|
│ ├── printer_service.py # Printer domain logic
|
|
│ ├── driver_service.py # Driver file management
|
|
│ └── package_builder.py # Orchestrates package generation
|
|
├── generators/
|
|
│ ├── ps_template.py # PowerShell script renderer
|
|
│ ├── intunewin_builder.py # .intunewin packaging (calls IntuneWinAppUtil)
|
|
│ └── zip_builder.py # NinjaRMM ZIP assembly
|
|
├── templates/
|
|
│ └── printer_install.ps1.j2 # Jinja2 PS script template
|
|
├── db/
|
|
│ ├── database.py # SQLite connection, migrations
|
|
│ └── models.py # Printer, Client, Driver models
|
|
├── storage/
|
|
│ └── driver_store.py # Read/write to volume-mounted driver path
|
|
├── config.py # Env-var driven configuration
|
|
├── main.py # App entrypoint, mounts routes
|
|
├── Dockerfile
|
|
└── docker-compose.yml
|
|
```
|
|
|
|
### Structure Rationale
|
|
|
|
- **api/:** Thin handlers only — validation and delegation to services. No business logic here.
|
|
- **services/:** All domain logic lives here, testable without HTTP context.
|
|
- **generators/:** Each output format is isolated. Adding a new format (SCCM, PDQ) means adding one file here.
|
|
- **templates/:** PS script is a template file, not a string in code. Easier to edit, diff, and review.
|
|
- **db/:** All persistence in one place. SQLite means no daemon dependency.
|
|
- **storage/:** Abstracts the filesystem from the rest of the app; makes future swap to S3 straightforward.
|
|
|
|
## Architectural Patterns
|
|
|
|
### Pattern 1: Request-Generate-Stream (synchronous generation)
|
|
|
|
**What:** The browser POSTs printer config → server assembles files → server streams the archive back as a file download, all in one HTTP call.
|
|
**When to use:** Package generation completes in under ~10 seconds. For this domain (small driver bundles, scripted packaging), synchronous is correct.
|
|
**Trade-offs:** Simple — no job queue, no polling, no WebSocket. The downside is the HTTP connection stays open during generation; acceptable for internal tooling on a LAN.
|
|
|
|
**Example flow:**
|
|
```
|
|
POST /api/packages/generate
|
|
body: { printerId, format: "intunewin" | "ninja" }
|
|
→ Package Builder assembles temp workspace
|
|
→ PS Template Engine renders install.ps1
|
|
→ IntuneWin Builder or ZIP Builder runs
|
|
→ Response: file download (Content-Disposition: attachment)
|
|
→ Temp workspace deleted after send
|
|
```
|
|
|
|
### Pattern 2: Content-Addressed Driver Storage
|
|
|
|
**What:** When a driver archive is uploaded, compute a SHA256 of the content and store it under that hash as the filename. Record the hash + original name in SQLite. Multiple printers referencing the same driver point to one file.
|
|
**When to use:** Always — avoids duplicate large files on the volume and makes driver references stable across renames.
|
|
**Trade-offs:** Slight upload cost for hashing. Cleanup requires reference counting (a row delete check before GC).
|
|
|
|
### Pattern 3: Template-Driven Script Generation
|
|
|
|
**What:** PowerShell install script is a Jinja2 (Python) or Handlebars/nunjucks (Node) template. Printer parameters are injected as variables. The template handles the elevation logic, port setup, and driver install commands in one canonical place.
|
|
**When to use:** Always — never build the PS script by string concatenation in code. The template is testable, auditable, and editable without touching Python/Node.
|
|
**Trade-offs:** Adds a template engine dependency (Jinja2 is stdlib-adjacent in Python; nunjucks is tiny in Node).
|
|
|
|
**Template sketch:**
|
|
```powershell
|
|
# printer_install.ps1.j2
|
|
$PrinterName = "{{ printer.name }}"
|
|
$PrinterIP = "{{ printer.ip }}"
|
|
$DriverName = "{{ printer.driver_name }}"
|
|
$PortName = "IP_{{ printer.ip }}"
|
|
|
|
# Elevation guard
|
|
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
|
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
|
|
exit
|
|
}
|
|
# ... driver install, port add, printer add ...
|
|
```
|
|
|
|
### Pattern 4: Subprocess Isolation for IntuneWinAppUtil
|
|
|
|
**What:** IntuneWinAppUtil.exe is a Windows binary. In a Linux container it must run under Wine, or the container must be Windows-based. Isolate this call behind an interface so the builder can be swapped (e.g., native reimplementation, different tool) without touching the orchestrator.
|
|
**When to use:** Whenever calling an external binary for a critical generation step.
|
|
**Trade-offs:** Wine on Linux adds image size (~500 MB). A Windows-based container image avoids Wine but is heavier by default. A pure Python reimplementation of the .intunewin format (outer ZIP + encrypted inner ZIP + detection.xml) is feasible and removes the tool dependency entirely — recommended if the format can be locked down to what Intune actually requires.
|
|
|
|
## Data Flow
|
|
|
|
### Printer Package Generation Flow
|
|
|
|
```
|
|
[Technician fills form]
|
|
↓
|
|
POST /api/packages/generate { printerId, format }
|
|
↓
|
|
API Layer validates request, loads printer config from SQLite
|
|
↓
|
|
Package Builder Service
|
|
├── Fetches driver files from Driver Store (volume path)
|
|
├── Renders install.ps1 via PS Template Engine (printer params injected)
|
|
├── Copies icon file (if Intune format)
|
|
└── Calls appropriate generator:
|
|
├── [Intune] IntuneWin Builder
|
|
│ └── subprocess: IntuneWinAppUtil.exe -c <tmpdir> -s install.ps1 -o <out>
|
|
│ OR: native Python .intunewin assembler
|
|
└── [Ninja] ZIP Builder
|
|
└── zipfile: { install.ps1, drivers/, readme.txt }
|
|
↓
|
|
Generated file written to temp workspace
|
|
↓
|
|
HTTP response streams file to browser (Content-Disposition: attachment)
|
|
↓
|
|
Temp workspace cleaned up (finally block / background task)
|
|
```
|
|
|
|
### Driver Upload Flow
|
|
|
|
```
|
|
[Technician uploads ZIP/INF]
|
|
↓
|
|
POST /api/drivers (multipart/form-data)
|
|
↓
|
|
Driver Service
|
|
├── Streams upload to temp file
|
|
├── Computes SHA256
|
|
├── Checks SQLite: already exists? → return existing record
|
|
├── Moves file to drivers volume at hash-named path
|
|
└── Inserts driver record (id, hash, original_name, size, uploaded_at)
|
|
↓
|
|
Response: driver record (id, name) for use in printer config
|
|
```
|
|
|
|
### Printer Config Flow
|
|
|
|
```
|
|
[Technician submits printer form]
|
|
↓
|
|
POST /api/printers
|
|
body: { name, ip, clientId, driverId, color, duplex, paperSize, tray, iconId }
|
|
↓
|
|
Printer Service validates params, resolves driverId FK
|
|
↓
|
|
SQLite INSERT → printer record with all settings
|
|
↓
|
|
Response: printer record
|
|
```
|
|
|
|
## Build Order (Component Dependencies)
|
|
|
|
Build these layers in order — each depends on the previous being stable:
|
|
|
|
1. **Data layer** — SQLite schema, models, connection management. Everything else reads/writes here.
|
|
2. **Storage layer** — Driver Store abstraction over the volume filesystem.
|
|
3. **Services (Printer + Driver)** — CRUD operations, driver upload with deduplication.
|
|
4. **PS Template Engine** — Standalone, can be developed and unit-tested in isolation.
|
|
5. **ZIP Builder (NinjaRMM)** — Depends on Driver Store + PS Template Engine. Simple to implement, good validation target.
|
|
6. **IntuneWin Builder** — Depends on PS Template Engine + icon handling. Most complex generator; validate ZIP Builder first.
|
|
7. **API Layer** — Thin wrappers once services are solid.
|
|
8. **UI** — Depends on all API routes being defined.
|
|
|
|
## Anti-Patterns
|
|
|
|
### Anti-Pattern 1: Generating Scripts via String Concatenation
|
|
|
|
**What people do:** Build the PowerShell script by concatenating strings in Python/Node code.
|
|
**Why it's wrong:** Injection risk (printer names with quotes, special chars), hard to read, impossible to review, breaks easily.
|
|
**Do this instead:** Use a template file (Jinja2 / nunjucks). Escape all user-provided values explicitly in the template context. Test the rendered output for known edge cases.
|
|
|
|
### Anti-Pattern 2: Storing Driver Blobs in SQLite
|
|
|
|
**What people do:** Base64-encode driver ZIPs and store them in a BLOB column.
|
|
**Why it's wrong:** SQLite performs poorly with large BLOBs; the database file balloons; backups become unwieldy; streaming is impossible.
|
|
**Do this instead:** Store files on the filesystem (Docker volume). Store only the path/hash reference in SQLite.
|
|
|
|
### Anti-Pattern 3: Blocking the Server During Package Generation
|
|
|
|
**What people do:** Call IntuneWinAppUtil.exe synchronously in the request handler with no timeout.
|
|
**Why it's wrong:** If generation hangs (tool crash, permission issue), the HTTP worker is permanently blocked. On a single-worker server this freezes the entire app.
|
|
**Do this instead:** Run the subprocess with a timeout (e.g., 60s). Use a thread pool or async subprocess so other requests can proceed. Return a 500 with a clear error if timeout is exceeded.
|
|
|
|
### Anti-Pattern 4: Leaving Temp Workspaces on Disk
|
|
|
|
**What people do:** Write package files to a temp directory and never clean up.
|
|
**Why it's wrong:** Disk fills up. Sensitive driver files and generated scripts accumulate indefinitely.
|
|
**Do this instead:** Use a try/finally pattern: generate in a uniquely-named temp dir, stream the response, then unconditionally delete the temp dir. For async flows, attach cleanup to the download-complete event.
|
|
|
|
### Anti-Pattern 5: Hardcoding the IntuneWinAppUtil Path
|
|
|
|
**What people do:** Hardcode `/app/tools/IntuneWinAppUtil.exe` in the generator code.
|
|
**Why it's wrong:** Breaks if the tool is moved, the container is Windows vs Linux with Wine, or the tool is replaced.
|
|
**Do this instead:** Configure via an environment variable (`INTUNEWIN_TOOL_PATH`). The builder service reads from config. Switching to a native Python reimplementation means changing only config, not code.
|
|
|
|
## Integration Points
|
|
|
|
### External Tooling
|
|
|
|
| Tool | Integration Pattern | Notes |
|
|
|------|--------------------|-------|
|
|
| IntuneWinAppUtil.exe | subprocess call with timeout | Must be bundled in container image. On Linux, requires Windows container base image OR Wine. Wine image ~500 MB extra. Consider native reimplementation instead. |
|
|
| PowerShell (target endpoints) | Template output only — no runtime PS in container | Container never executes PS; it only generates .ps1 files. |
|
|
|
|
### Internal Boundaries
|
|
|
|
| Boundary | Communication | Notes |
|
|
|----------|--------------|-------|
|
|
| API Layer ↔ Services | Direct function call (same process) | No IPC needed; single container, single process. |
|
|
| Services ↔ DB | SQLite via ORM or raw queries | Use WAL mode for concurrent reads during long-running generates. |
|
|
| Services ↔ Driver Store | Filesystem reads via Driver Store abstraction | Abstraction allows future swap to S3 without service changes. |
|
|
| Package Builder ↔ Generators | Direct function call, returns file path | Each generator writes output to a caller-provided temp directory. |
|
|
| API Layer ↔ Temp Files | Generator returns path → API streams file → cleanup | Use streaming response to avoid loading entire archive into memory. |
|
|
|
|
## Scaling Considerations
|
|
|
|
This is an internal MSP tool. Scaling expectations are low; design for correctness and maintainability, not throughput.
|
|
|
|
| Scale | Architecture Adjustments |
|
|
|-------|--------------------------|
|
|
| 1-5 concurrent technicians | Default single-worker setup is fine. Use thread pool for subprocess calls. SQLite WAL mode handles concurrent reads. |
|
|
| 10+ concurrent technicians | Add Gunicorn/Uvicorn worker count (still single container). Background job queue (in-process, e.g. Python's concurrent.futures) if generation is slow. |
|
|
| Multi-tenant or SaaS | Out of scope. Would require auth, per-tenant isolation, external storage — full redesign. |
|
|
|
|
### Scaling Priorities
|
|
|
|
1. **First bottleneck:** IntuneWinAppUtil.exe process duration (CPU/disk). Mitigation: timeout + thread pool.
|
|
2. **Second bottleneck:** Disk I/O on the Docker volume for large driver packages. Mitigation: content-addressing avoids redundant copies.
|
|
|
|
## Sources
|
|
|
|
- [Microsoft: .intunewin format internals — How to decode Intune Win32 App Packages](https://msendpointmgr.com/2019/01/18/how-to-decode-intune-win32-app-packages/)
|
|
- [Microsoft Learn: Prepare a Win32 App for Intune (.intunewin packaging)](https://learn.microsoft.com/en-us/intune/intune-service/apps/apps-win32-prepare)
|
|
- [Microsoft Win32 Content Prep Tool (IntuneWinAppUtil) — GitHub](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
|
|
- [FastAPI deployment in Docker containers](https://fastapi.tiangolo.com/deployment/docker/)
|
|
- [FastAPI file upload handling](https://betterstack.com/community/guides/scaling-python/uploading-files-using-fastapi/)
|
|
- [Web-Queue-Worker Architecture Style — Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/web-queue-worker)
|
|
- [PowerShell as Win32 installer in Intune (2026)](https://headsinthecloud.blog/2026/02/24/from-packaging-to-logic-powershell-as-the-new-win32-installer-in-intune/)
|
|
- [NinjaOne Remote Script Deployment](https://www.ninjaone.com/remote-script-deployment/)
|
|
|
|
---
|
|
*Architecture research for: ImpTune — single-container printer deployment package generator*
|
|
*Researched: 2026-04-10*
|