Files
2026-04-15 17:57:12 +02:00

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*