docs: complete project research

Adds STACK, FEATURES, ARCHITECTURE, PITFALLS, and SUMMARY research files
covering the full ImpTune technology stack, feature set, architecture
patterns, and critical pitfalls for the printer deployment package generator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 10:34:25 +02:00
co-authored by Claude Sonnet 4.6
parent 67a54673e2
commit 59c384ed13
4 changed files with 1026 additions and 0 deletions
+324
View File
@@ -0,0 +1,324 @@
# 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*