GitHub
08/11/2026, 11:55 AM.deb, .rpm, .pkg, or .msi packages — each targeting a different architecture (amd64, arm64, x86_64, aarch64, universal).
Closes #466.
Problem
The TLSEnvironment model stored one package URL per type (DebPackage, RpmPackage, MsiPackage, PkgPackage). An operator with both amd64 and arm64 Linux nodes needed two .deb and two .rpm packages, but the model only had room for one of each — no way to distinguish architecture.
Changes
New model (pkg/environments/packages.go)
• EnvironmentPackage table — one row per (environment, type, architecture, URL) with is_default flag
• CRUD methods: GetPackages, GetPackagesByType, GetPackage (with fallback chain: exact architecture → default → first available), AddPackage, RemovePackage, SetDefaultPackage
• MigrateLegacyPackages — idempotent migration that copies existing single-package fields into the new table on first boot
• Architecture validation (ValidArchitectures): amd64, arm64, x86_64, aarch64, universal
Auto-migrate (pkg/environments/environments.go)
• CreateEnvironment now auto-migrates EnvironmentPackage and runs the legacy migration
TLS download handler (cmd/tls/handlers/post.go)
• Checks the EnvironmentPackage table first (by type + arch), falls back to legacy single-package fields
• New route: GET /{env}/{secretpath}/package/{package}/{arch} — serves architecture-specific packages
• Old route: GET /{env}/{secretpath}/package/{package} — still works (backward compat)
API handlers (cmd/api/handlers/environments_packages.go)
| Method | Path | Description |
| ------ | ---------------------------------------- | ------------------------------------------- |
| GET | /api/v1/environments/{env}/packages | List all packages |
| POST | /api/v1/environments/{env}/packages | Add a package (type, arch, url, is_default) |
| DELETE | /api/v1/environments/{env}/packages/{id} | Remove a package |
All require AdminLevel permissions and are audit-logged.
Frontend (frontend/src/features/enrollment/EnrollPage.tsx)
• New PackageListCard component below the existing PackageUrlCard
• Shows all multi-architecture packages grouped by type (DEB / RPM / PKG / MSI)
• Add form with type selector, architecture selector, and URL input
• Remove button per package with is_default badge
• API client functions: listEnvPackages, addEnvPackage, removeEnvPackage in frontend/src/api/environments.ts
Tests (10 new in pkg/environments/packages_test.go)
• TestAddPackage_Success — add two packages (amd64 + arm64) for the same type
• TestAddPackage_InvalidArchitecture — rejects unknown architecture
• TestGetPackage_ByArchitecture — exact architecture match
• TestGetPackage_FallbackToDefault — falls back to is_default package when arch not found
• TestGetPackage_FallbackToFirstAvailable — falls back to first by architecture order when no default
• TestRemovePackage_Success — removes a package by ID
• TestRemovePackage_NotFound — returns ErrRecordNotFound for missing ID
• TestSetDefaultPackage — sets a new default and unsets the old one
• TestMigrateLegacyPackages — migrates `DebPackage`/`RpmPackage` fields into the new table
• TestMigrateLegacyPackages_Idempotent — running migration twice doesn't duplicate rows
Backward compatibility
• The legacy `DebPackage`/`RpmPackage`/`MsiPackage`/`PkgPackage` fields and their Update*Package methods are unchanged
• The old TLS route (without {arch}) still works — serves from the new table or falls back to legacy fields
• The legacy PackageUrlCard in the frontend is preserved alongside the new PackageListCard
• Migration is idempotent — existing deployments are transparently upgraded on next boot
Validation
• go build ./... — clean
• go test ./... — all packages pass (10 new tests)
• golangci-lint run ./pkg/environments/... — 0 issues
• npm run check — TypeScript typecheck clean
• npm test — 34 test files / 209 tests pass
• gofmt clean on all modified files
jmpsec/osctrlGitHub
08/11/2026, 12:00 PM