Skip to content

Repository files navigation

UVITRepo — Munki repo plugins for UVIT

Repo plugins that let admins push packages directly into UVIT using munkiimport and AutoPkg's MunkiImporter processor. Two plugins, same API and configuration:

  • UVITRepo.plugin (Swift) — for Munki 7's Swift tools (munkiimport etc.), which load dylib plugins from /usr/local/munki/repoplugins/. Requires Munki 7.0 or later.
  • UVITRepo.py (Python) — for AutoPkg's MunkiImporter processor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at /usr/local/munki/munkilib/munkirepo/UVITRepo.py.

Installation

Download the latest UVITRepo-<version>.pkg from the Releases page and install it:

sudo installer -pkg UVITRepo-1.0.0.pkg -target /

This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.

Configuration

1. Get an API token

Log in to the UVIT console and go to My Account → API Tokens → Create Token (/myaccount/tokens). Copy the generated token — it looks like uvit_pat_…. This is your UVIT_TOKEN.

The token identifies you as a user. The server checks your membership for the target you specify (UVIT_TARGET); it does not read a tenant or repo from the token itself.

2. Find your target and repo ID

  • UVIT_TARGET: global if you are a sysadmin pushing to the global scope, or tenant:<id> (e.g. tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.

  • UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.

3. Configure munkiimport

munkiimport --configure

Set the following values when prompted:

SettingValue
repo_urlhttps://<your-console>/api/repo
pluginUVITRepo

Example:

repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo

4. Supply credentials

Option A — environment variables (session-scoped):

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmg

Option B — macOS admin preferences (persistent, recommended for workstations):

sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"

Read them back to verify:

sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimport

Usage

munkiimport /path/to/GoogleChrome.dmg

munkiimport will:

  1. Upload the installer to the UVIT S3 repo (PUT /api/repo/pkgs/…).
  2. Upload the generated pkginfo plist (PUT /api/repo/pkgsinfo/…).
  3. Call makecatalogs — this is a no-op on the UVIT side (catalogs are built dynamically from the database).

The package appears in the UVIT console immediately after a successful push.

AutoPkg

AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a bare CI runner just copy the file into /usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too: MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python munkilib comes with Munki's compatibility component package.

Add these variables to your AutoPkg preferences or pass them on the command line:

<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>

Set the UVIT variables in the environment before running AutoPkg:

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munki

For unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.

Environment variables / preference keys

Env varPref keyRequiredDescription
UVIT_TOKENuvitTokenYesOpaque API token from My Account → API Tokens (uvit_pat_…).
UVIT_TARGETuvitTargetYesglobal or tenant:<id>. Sent on every request. The server
resolves the tenant from this value and checks membership.
UVIT_REPO_IDuvitRepoIDFor writesNumeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo.

Preferences domain: ch.eigercode.uvit.munkiimport

Endpoint mapping

Munki callUVIT API endpointHeaders
list("pkgsinfo")GET <repo_url>/pkgsinfoAuth + Target
list(<other kind>)returns [] (see Known Limitations below)
get("pkgsinfo/<name>/<ver>.plist")GET <repo_url>/pkgsinfo/<name>/<ver>.plistAuth + Target
get("pkgs/<path>")GET <repo_url>/pkgs/<path> → 307 to S3Auth + Target
put("pkgs/<path>", fromFile:)PUT <repo_url>/pkgs/<path> → 307 to S3Auth + Target + Repo-ID
put("pkgsinfo/<path>", content:)PUT <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgsinfo/<path>")DELETE <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgs/<path>")DELETE <repo_url>/pkgs/<path>Auth + Target + Repo-ID
pathFor(_)nil (non-filesystem repo)
makecatalogs (called by Munki tools)POST <repo_url>/makecatalogs → no-op 200Auth + Target

Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>, X-UVIT-Repo-ID: <repoID>.

Presigned pkg uploads (307 redirect)

Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a presigned S3 PUT URL and re-send the file bytes there, without forwarding the Authorization/X-UVIT-* headers (the presigned URL carries its own auth). An older console that predates this (pre-uvit-console#575) never sends a redirect on that PUT, so both plugins keep working against it unchanged.

Known limitations

dylib code-signing / Library Validation (Swift plugin only; must verify on a real Mac)

This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg runs are unaffected — they use the Python plugin, which involves no dylib loading.

macOS Library Validation requires that a dylib loaded by a hardened process either shares the same Apple Developer Team ID as the host binary, or that the host binary carries the com.apple.security.cs.disable-library-validation entitlement.

Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.

This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:

  1. File a Munki issue requesting com.apple.security.cs.disable-library-validation be added to Munki's entitlements (the correct long-term fix).
  2. Use an ad-hoc or unsigned build of the plugin for internal testing only.

catalogs / manifests / icons list not implemented server-side

list("catalogs"), list("manifests"), and list("icons") return empty lists. This means:

  • makecatalogs (separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.
  • iconimporter cannot sync icons via this API.

These require server-side list endpoints that are planned as a follow-up.

Building from source

Requires Xcode 16+ and macOS 12+.

xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath build

Or use the convenience script (also creates a .pkg):

./build_pkg.sh

License

MIT — see LICENSE.

About

Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - EigerCode/uvit-munkiimport-plugin: Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter. · GitHub
Skip to content

Repository files navigation

UVITRepo — Munki repo plugins for UVIT

Repo plugins that let admins push packages directly into UVIT using munkiimport and AutoPkg's MunkiImporter processor. Two plugins, same API and configuration:

  • UVITRepo.plugin (Swift) — for Munki 7's Swift tools (munkiimport etc.), which load dylib plugins from /usr/local/munki/repoplugins/. Requires Munki 7.0 or later.
  • UVITRepo.py (Python) — for AutoPkg's MunkiImporter processor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at /usr/local/munki/munkilib/munkirepo/UVITRepo.py.

Installation

Download the latest UVITRepo-<version>.pkg from the Releases page and install it:

sudo installer -pkg UVITRepo-1.0.0.pkg -target /

This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.

Configuration

1. Get an API token

Log in to the UVIT console and go to My Account → API Tokens → Create Token (/myaccount/tokens). Copy the generated token — it looks like uvit_pat_…. This is your UVIT_TOKEN.

The token identifies you as a user. The server checks your membership for the target you specify (UVIT_TARGET); it does not read a tenant or repo from the token itself.

2. Find your target and repo ID

  • UVIT_TARGET: global if you are a sysadmin pushing to the global scope, or tenant:<id> (e.g. tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.

  • UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.

3. Configure munkiimport

munkiimport --configure

Set the following values when prompted:

SettingValue
repo_urlhttps://<your-console>/api/repo
pluginUVITRepo

Example:

repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo

4. Supply credentials

Option A — environment variables (session-scoped):

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmg

Option B — macOS admin preferences (persistent, recommended for workstations):

sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"

Read them back to verify:

sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimport

Usage

munkiimport /path/to/GoogleChrome.dmg

munkiimport will:

  1. Upload the installer to the UVIT S3 repo (PUT /api/repo/pkgs/…).
  2. Upload the generated pkginfo plist (PUT /api/repo/pkgsinfo/…).
  3. Call makecatalogs — this is a no-op on the UVIT side (catalogs are built dynamically from the database).

The package appears in the UVIT console immediately after a successful push.

AutoPkg

AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a bare CI runner just copy the file into /usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too: MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python munkilib comes with Munki's compatibility component package.

Add these variables to your AutoPkg preferences or pass them on the command line:

<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>

Set the UVIT variables in the environment before running AutoPkg:

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munki

For unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.

Environment variables / preference keys

Env varPref keyRequiredDescription
UVIT_TOKENuvitTokenYesOpaque API token from My Account → API Tokens (uvit_pat_…).
UVIT_TARGETuvitTargetYesglobal or tenant:<id>. Sent on every request. The server
resolves the tenant from this value and checks membership.
UVIT_REPO_IDuvitRepoIDFor writesNumeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo.

Preferences domain: ch.eigercode.uvit.munkiimport

Endpoint mapping

Munki callUVIT API endpointHeaders
list("pkgsinfo")GET <repo_url>/pkgsinfoAuth + Target
list(<other kind>)returns [] (see Known Limitations below)
get("pkgsinfo/<name>/<ver>.plist")GET <repo_url>/pkgsinfo/<name>/<ver>.plistAuth + Target
get("pkgs/<path>")GET <repo_url>/pkgs/<path> → 307 to S3Auth + Target
put("pkgs/<path>", fromFile:)PUT <repo_url>/pkgs/<path> → 307 to S3Auth + Target + Repo-ID
put("pkgsinfo/<path>", content:)PUT <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgsinfo/<path>")DELETE <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgs/<path>")DELETE <repo_url>/pkgs/<path>Auth + Target + Repo-ID
pathFor(_)nil (non-filesystem repo)
makecatalogs (called by Munki tools)POST <repo_url>/makecatalogs → no-op 200Auth + Target

Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>, X-UVIT-Repo-ID: <repoID>.

Presigned pkg uploads (307 redirect)

Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a presigned S3 PUT URL and re-send the file bytes there, without forwarding the Authorization/X-UVIT-* headers (the presigned URL carries its own auth). An older console that predates this (pre-uvit-console#575) never sends a redirect on that PUT, so both plugins keep working against it unchanged.

Known limitations

dylib code-signing / Library Validation (Swift plugin only; must verify on a real Mac)

This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg runs are unaffected — they use the Python plugin, which involves no dylib loading.

macOS Library Validation requires that a dylib loaded by a hardened process either shares the same Apple Developer Team ID as the host binary, or that the host binary carries the com.apple.security.cs.disable-library-validation entitlement.

Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.

This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:

  1. File a Munki issue requesting com.apple.security.cs.disable-library-validation be added to Munki's entitlements (the correct long-term fix).
  2. Use an ad-hoc or unsigned build of the plugin for internal testing only.

catalogs / manifests / icons list not implemented server-side

list("catalogs"), list("manifests"), and list("icons") return empty lists. This means:

  • makecatalogs (separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.
  • iconimporter cannot sync icons via this API.

These require server-side list endpoints that are planned as a follow-up.

Building from source

Requires Xcode 16+ and macOS 12+.

xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath build

Or use the convenience script (also creates a .pkg):

./build_pkg.sh

License

MIT — see LICENSE.

About

Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - EigerCode/uvit-munkiimport-plugin: Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter. · GitHub
Skip to content

Repository files navigation

UVITRepo — Munki repo plugins for UVIT

Repo plugins that let admins push packages directly into UVIT using munkiimport and AutoPkg's MunkiImporter processor. Two plugins, same API and configuration:

  • UVITRepo.plugin (Swift) — for Munki 7's Swift tools (munkiimport etc.), which load dylib plugins from /usr/local/munki/repoplugins/. Requires Munki 7.0 or later.
  • UVITRepo.py (Python) — for AutoPkg's MunkiImporter processor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at /usr/local/munki/munkilib/munkirepo/UVITRepo.py.

Installation

Download the latest UVITRepo-<version>.pkg from the Releases page and install it:

sudo installer -pkg UVITRepo-1.0.0.pkg -target /

This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.

Configuration

1. Get an API token

Log in to the UVIT console and go to My Account → API Tokens → Create Token (/myaccount/tokens). Copy the generated token — it looks like uvit_pat_…. This is your UVIT_TOKEN.

The token identifies you as a user. The server checks your membership for the target you specify (UVIT_TARGET); it does not read a tenant or repo from the token itself.

2. Find your target and repo ID

  • UVIT_TARGET: global if you are a sysadmin pushing to the global scope, or tenant:<id> (e.g. tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.

  • UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.

3. Configure munkiimport

munkiimport --configure

Set the following values when prompted:

SettingValue
repo_urlhttps://<your-console>/api/repo
pluginUVITRepo

Example:

repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo

4. Supply credentials

Option A — environment variables (session-scoped):

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmg

Option B — macOS admin preferences (persistent, recommended for workstations):

sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"

Read them back to verify:

sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimport

Usage

munkiimport /path/to/GoogleChrome.dmg

munkiimport will:

  1. Upload the installer to the UVIT S3 repo (PUT /api/repo/pkgs/…).
  2. Upload the generated pkginfo plist (PUT /api/repo/pkgsinfo/…).
  3. Call makecatalogs — this is a no-op on the UVIT side (catalogs are built dynamically from the database).

The package appears in the UVIT console immediately after a successful push.

AutoPkg

AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a bare CI runner just copy the file into /usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too: MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python munkilib comes with Munki's compatibility component package.

Add these variables to your AutoPkg preferences or pass them on the command line:

<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>

Set the UVIT variables in the environment before running AutoPkg:

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munki

For unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.

Environment variables / preference keys

Env varPref keyRequiredDescription
UVIT_TOKENuvitTokenYesOpaque API token from My Account → API Tokens (uvit_pat_…).
UVIT_TARGETuvitTargetYesglobal or tenant:<id>. Sent on every request. The server
resolves the tenant from this value and checks membership.
UVIT_REPO_IDuvitRepoIDFor writesNumeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo.

Preferences domain: ch.eigercode.uvit.munkiimport

Endpoint mapping

Munki callUVIT API endpointHeaders
list("pkgsinfo")GET <repo_url>/pkgsinfoAuth + Target
list(<other kind>)returns [] (see Known Limitations below)
get("pkgsinfo/<name>/<ver>.plist")GET <repo_url>/pkgsinfo/<name>/<ver>.plistAuth + Target
get("pkgs/<path>")GET <repo_url>/pkgs/<path> → 307 to S3Auth + Target
put("pkgs/<path>", fromFile:)PUT <repo_url>/pkgs/<path> → 307 to S3Auth + Target + Repo-ID
put("pkgsinfo/<path>", content:)PUT <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgsinfo/<path>")DELETE <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgs/<path>")DELETE <repo_url>/pkgs/<path>Auth + Target + Repo-ID
pathFor(_)nil (non-filesystem repo)
makecatalogs (called by Munki tools)POST <repo_url>/makecatalogs → no-op 200Auth + Target

Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>, X-UVIT-Repo-ID: <repoID>.

Presigned pkg uploads (307 redirect)

Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a presigned S3 PUT URL and re-send the file bytes there, without forwarding the Authorization/X-UVIT-* headers (the presigned URL carries its own auth). An older console that predates this (pre-uvit-console#575) never sends a redirect on that PUT, so both plugins keep working against it unchanged.

Known limitations

dylib code-signing / Library Validation (Swift plugin only; must verify on a real Mac)

This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg runs are unaffected — they use the Python plugin, which involves no dylib loading.

macOS Library Validation requires that a dylib loaded by a hardened process either shares the same Apple Developer Team ID as the host binary, or that the host binary carries the com.apple.security.cs.disable-library-validation entitlement.

Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.

This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:

  1. File a Munki issue requesting com.apple.security.cs.disable-library-validation be added to Munki's entitlements (the correct long-term fix).
  2. Use an ad-hoc or unsigned build of the plugin for internal testing only.

catalogs / manifests / icons list not implemented server-side

list("catalogs"), list("manifests"), and list("icons") return empty lists. This means:

  • makecatalogs (separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.
  • iconimporter cannot sync icons via this API.

These require server-side list endpoints that are planned as a follow-up.

Building from source

Requires Xcode 16+ and macOS 12+.

xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath build

Or use the convenience script (also creates a .pkg):

./build_pkg.sh

License

MIT — see LICENSE.

About

Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - EigerCode/uvit-munkiimport-plugin: Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter. · GitHub
Skip to content

Repository files navigation

UVITRepo — Munki repo plugins for UVIT

Repo plugins that let admins push packages directly into UVIT using munkiimport and AutoPkg's MunkiImporter processor. Two plugins, same API and configuration:

  • UVITRepo.plugin (Swift) — for Munki 7's Swift tools (munkiimport etc.), which load dylib plugins from /usr/local/munki/repoplugins/. Requires Munki 7.0 or later.
  • UVITRepo.py (Python) — for AutoPkg's MunkiImporter processor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at /usr/local/munki/munkilib/munkirepo/UVITRepo.py.

Installation

Download the latest UVITRepo-<version>.pkg from the Releases page and install it:

sudo installer -pkg UVITRepo-1.0.0.pkg -target /

This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.

Configuration

1. Get an API token

Log in to the UVIT console and go to My Account → API Tokens → Create Token (/myaccount/tokens). Copy the generated token — it looks like uvit_pat_…. This is your UVIT_TOKEN.

The token identifies you as a user. The server checks your membership for the target you specify (UVIT_TARGET); it does not read a tenant or repo from the token itself.

2. Find your target and repo ID

  • UVIT_TARGET: global if you are a sysadmin pushing to the global scope, or tenant:<id> (e.g. tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.

  • UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.

3. Configure munkiimport

munkiimport --configure

Set the following values when prompted:

SettingValue
repo_urlhttps://<your-console>/api/repo
pluginUVITRepo

Example:

repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo

4. Supply credentials

Option A — environment variables (session-scoped):

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmg

Option B — macOS admin preferences (persistent, recommended for workstations):

sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"

Read them back to verify:

sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimport

Usage

munkiimport /path/to/GoogleChrome.dmg

munkiimport will:

  1. Upload the installer to the UVIT S3 repo (PUT /api/repo/pkgs/…).
  2. Upload the generated pkginfo plist (PUT /api/repo/pkgsinfo/…).
  3. Call makecatalogs — this is a no-op on the UVIT side (catalogs are built dynamically from the database).

The package appears in the UVIT console immediately after a successful push.

AutoPkg

AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a bare CI runner just copy the file into /usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too: MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python munkilib comes with Munki's compatibility component package.

Add these variables to your AutoPkg preferences or pass them on the command line:

<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>

Set the UVIT variables in the environment before running AutoPkg:

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munki

For unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.

Environment variables / preference keys

Env varPref keyRequiredDescription
UVIT_TOKENuvitTokenYesOpaque API token from My Account → API Tokens (uvit_pat_…).
UVIT_TARGETuvitTargetYesglobal or tenant:<id>. Sent on every request. The server
resolves the tenant from this value and checks membership.
UVIT_REPO_IDuvitRepoIDFor writesNumeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo.

Preferences domain: ch.eigercode.uvit.munkiimport

Endpoint mapping

Munki callUVIT API endpointHeaders
list("pkgsinfo")GET <repo_url>/pkgsinfoAuth + Target
list(<other kind>)returns [] (see Known Limitations below)
get("pkgsinfo/<name>/<ver>.plist")GET <repo_url>/pkgsinfo/<name>/<ver>.plistAuth + Target
get("pkgs/<path>")GET <repo_url>/pkgs/<path> → 307 to S3Auth + Target
put("pkgs/<path>", fromFile:)PUT <repo_url>/pkgs/<path> → 307 to S3Auth + Target + Repo-ID
put("pkgsinfo/<path>", content:)PUT <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgsinfo/<path>")DELETE <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgs/<path>")DELETE <repo_url>/pkgs/<path>Auth + Target + Repo-ID
pathFor(_)nil (non-filesystem repo)
makecatalogs (called by Munki tools)POST <repo_url>/makecatalogs → no-op 200Auth + Target

Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>, X-UVIT-Repo-ID: <repoID>.

Presigned pkg uploads (307 redirect)

Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a presigned S3 PUT URL and re-send the file bytes there, without forwarding the Authorization/X-UVIT-* headers (the presigned URL carries its own auth). An older console that predates this (pre-uvit-console#575) never sends a redirect on that PUT, so both plugins keep working against it unchanged.

Known limitations

dylib code-signing / Library Validation (Swift plugin only; must verify on a real Mac)

This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg runs are unaffected — they use the Python plugin, which involves no dylib loading.

macOS Library Validation requires that a dylib loaded by a hardened process either shares the same Apple Developer Team ID as the host binary, or that the host binary carries the com.apple.security.cs.disable-library-validation entitlement.

Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.

This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:

  1. File a Munki issue requesting com.apple.security.cs.disable-library-validation be added to Munki's entitlements (the correct long-term fix).
  2. Use an ad-hoc or unsigned build of the plugin for internal testing only.

catalogs / manifests / icons list not implemented server-side

list("catalogs"), list("manifests"), and list("icons") return empty lists. This means:

  • makecatalogs (separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.
  • iconimporter cannot sync icons via this API.

These require server-side list endpoints that are planned as a follow-up.

Building from source

Requires Xcode 16+ and macOS 12+.

xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath build

Or use the convenience script (also creates a .pkg):

./build_pkg.sh

License

MIT — see LICENSE.

About

Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - EigerCode/uvit-munkiimport-plugin: Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter. · GitHub
Skip to content

Repository files navigation

UVITRepo — Munki repo plugins for UVIT

Repo plugins that let admins push packages directly into UVIT using munkiimport and AutoPkg's MunkiImporter processor. Two plugins, same API and configuration:

  • UVITRepo.plugin (Swift) — for Munki 7's Swift tools (munkiimport etc.), which load dylib plugins from /usr/local/munki/repoplugins/. Requires Munki 7.0 or later.
  • UVITRepo.py (Python) — for AutoPkg's MunkiImporter processor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at /usr/local/munki/munkilib/munkirepo/UVITRepo.py.

Installation

Download the latest UVITRepo-<version>.pkg from the Releases page and install it:

sudo installer -pkg UVITRepo-1.0.0.pkg -target /

This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.

Configuration

1. Get an API token

Log in to the UVIT console and go to My Account → API Tokens → Create Token (/myaccount/tokens). Copy the generated token — it looks like uvit_pat_…. This is your UVIT_TOKEN.

The token identifies you as a user. The server checks your membership for the target you specify (UVIT_TARGET); it does not read a tenant or repo from the token itself.

2. Find your target and repo ID

  • UVIT_TARGET: global if you are a sysadmin pushing to the global scope, or tenant:<id> (e.g. tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.

  • UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.

3. Configure munkiimport

munkiimport --configure

Set the following values when prompted:

SettingValue
repo_urlhttps://<your-console>/api/repo
pluginUVITRepo

Example:

repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo

4. Supply credentials

Option A — environment variables (session-scoped):

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmg

Option B — macOS admin preferences (persistent, recommended for workstations):

sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"

Read them back to verify:

sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimport

Usage

munkiimport /path/to/GoogleChrome.dmg

munkiimport will:

  1. Upload the installer to the UVIT S3 repo (PUT /api/repo/pkgs/…).
  2. Upload the generated pkginfo plist (PUT /api/repo/pkgsinfo/…).
  3. Call makecatalogs — this is a no-op on the UVIT side (catalogs are built dynamically from the database).

The package appears in the UVIT console immediately after a successful push.

AutoPkg

AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a bare CI runner just copy the file into /usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too: MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python munkilib comes with Munki's compatibility component package.

Add these variables to your AutoPkg preferences or pass them on the command line:

<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>

Set the UVIT variables in the environment before running AutoPkg:

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munki

For unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.

Environment variables / preference keys

Env varPref keyRequiredDescription
UVIT_TOKENuvitTokenYesOpaque API token from My Account → API Tokens (uvit_pat_…).
UVIT_TARGETuvitTargetYesglobal or tenant:<id>. Sent on every request. The server
resolves the tenant from this value and checks membership.
UVIT_REPO_IDuvitRepoIDFor writesNumeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo.

Preferences domain: ch.eigercode.uvit.munkiimport

Endpoint mapping

Munki callUVIT API endpointHeaders
list("pkgsinfo")GET <repo_url>/pkgsinfoAuth + Target
list(<other kind>)returns [] (see Known Limitations below)
get("pkgsinfo/<name>/<ver>.plist")GET <repo_url>/pkgsinfo/<name>/<ver>.plistAuth + Target
get("pkgs/<path>")GET <repo_url>/pkgs/<path> → 307 to S3Auth + Target
put("pkgs/<path>", fromFile:)PUT <repo_url>/pkgs/<path> → 307 to S3Auth + Target + Repo-ID
put("pkgsinfo/<path>", content:)PUT <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgsinfo/<path>")DELETE <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgs/<path>")DELETE <repo_url>/pkgs/<path>Auth + Target + Repo-ID
pathFor(_)nil (non-filesystem repo)
makecatalogs (called by Munki tools)POST <repo_url>/makecatalogs → no-op 200Auth + Target

Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>, X-UVIT-Repo-ID: <repoID>.

Presigned pkg uploads (307 redirect)

Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a presigned S3 PUT URL and re-send the file bytes there, without forwarding the Authorization/X-UVIT-* headers (the presigned URL carries its own auth). An older console that predates this (pre-uvit-console#575) never sends a redirect on that PUT, so both plugins keep working against it unchanged.

Known limitations

dylib code-signing / Library Validation (Swift plugin only; must verify on a real Mac)

This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg runs are unaffected — they use the Python plugin, which involves no dylib loading.

macOS Library Validation requires that a dylib loaded by a hardened process either shares the same Apple Developer Team ID as the host binary, or that the host binary carries the com.apple.security.cs.disable-library-validation entitlement.

Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.

This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:

  1. File a Munki issue requesting com.apple.security.cs.disable-library-validation be added to Munki's entitlements (the correct long-term fix).
  2. Use an ad-hoc or unsigned build of the plugin for internal testing only.

catalogs / manifests / icons list not implemented server-side

list("catalogs"), list("manifests"), and list("icons") return empty lists. This means:

  • makecatalogs (separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.
  • iconimporter cannot sync icons via this API.

These require server-side list endpoints that are planned as a follow-up.

Building from source

Requires Xcode 16+ and macOS 12+.

xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath build

Or use the convenience script (also creates a .pkg):

./build_pkg.sh

License

MIT — see LICENSE.

About

Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - EigerCode/uvit-munkiimport-plugin: Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter. · GitHub
Skip to content

Repository files navigation

UVITRepo — Munki repo plugins for UVIT

Repo plugins that let admins push packages directly into UVIT using munkiimport and AutoPkg's MunkiImporter processor. Two plugins, same API and configuration:

  • UVITRepo.plugin (Swift) — for Munki 7's Swift tools (munkiimport etc.), which load dylib plugins from /usr/local/munki/repoplugins/. Requires Munki 7.0 or later.
  • UVITRepo.py (Python) — for AutoPkg's MunkiImporter processor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at /usr/local/munki/munkilib/munkirepo/UVITRepo.py.

Installation

Download the latest UVITRepo-<version>.pkg from the Releases page and install it:

sudo installer -pkg UVITRepo-1.0.0.pkg -target /

This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.

Configuration

1. Get an API token

Log in to the UVIT console and go to My Account → API Tokens → Create Token (/myaccount/tokens). Copy the generated token — it looks like uvit_pat_…. This is your UVIT_TOKEN.

The token identifies you as a user. The server checks your membership for the target you specify (UVIT_TARGET); it does not read a tenant or repo from the token itself.

2. Find your target and repo ID

  • UVIT_TARGET: global if you are a sysadmin pushing to the global scope, or tenant:<id> (e.g. tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.

  • UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.

3. Configure munkiimport

munkiimport --configure

Set the following values when prompted:

SettingValue
repo_urlhttps://<your-console>/api/repo
pluginUVITRepo

Example:

repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo

4. Supply credentials

Option A — environment variables (session-scoped):

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmg

Option B — macOS admin preferences (persistent, recommended for workstations):

sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"

Read them back to verify:

sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimport

Usage

munkiimport /path/to/GoogleChrome.dmg

munkiimport will:

  1. Upload the installer to the UVIT S3 repo (PUT /api/repo/pkgs/…).
  2. Upload the generated pkginfo plist (PUT /api/repo/pkgsinfo/…).
  3. Call makecatalogs — this is a no-op on the UVIT side (catalogs are built dynamically from the database).

The package appears in the UVIT console immediately after a successful push.

AutoPkg

AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a bare CI runner just copy the file into /usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too: MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python munkilib comes with Munki's compatibility component package.

Add these variables to your AutoPkg preferences or pass them on the command line:

<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>

Set the UVIT variables in the environment before running AutoPkg:

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munki

For unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.

Environment variables / preference keys

Env varPref keyRequiredDescription
UVIT_TOKENuvitTokenYesOpaque API token from My Account → API Tokens (uvit_pat_…).
UVIT_TARGETuvitTargetYesglobal or tenant:<id>. Sent on every request. The server
resolves the tenant from this value and checks membership.
UVIT_REPO_IDuvitRepoIDFor writesNumeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo.

Preferences domain: ch.eigercode.uvit.munkiimport

Endpoint mapping

Munki callUVIT API endpointHeaders
list("pkgsinfo")GET <repo_url>/pkgsinfoAuth + Target
list(<other kind>)returns [] (see Known Limitations below)
get("pkgsinfo/<name>/<ver>.plist")GET <repo_url>/pkgsinfo/<name>/<ver>.plistAuth + Target
get("pkgs/<path>")GET <repo_url>/pkgs/<path> → 307 to S3Auth + Target
put("pkgs/<path>", fromFile:)PUT <repo_url>/pkgs/<path> → 307 to S3Auth + Target + Repo-ID
put("pkgsinfo/<path>", content:)PUT <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgsinfo/<path>")DELETE <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgs/<path>")DELETE <repo_url>/pkgs/<path>Auth + Target + Repo-ID
pathFor(_)nil (non-filesystem repo)
makecatalogs (called by Munki tools)POST <repo_url>/makecatalogs → no-op 200Auth + Target

Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>, X-UVIT-Repo-ID: <repoID>.

Presigned pkg uploads (307 redirect)

Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a presigned S3 PUT URL and re-send the file bytes there, without forwarding the Authorization/X-UVIT-* headers (the presigned URL carries its own auth). An older console that predates this (pre-uvit-console#575) never sends a redirect on that PUT, so both plugins keep working against it unchanged.

Known limitations

dylib code-signing / Library Validation (Swift plugin only; must verify on a real Mac)

This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg runs are unaffected — they use the Python plugin, which involves no dylib loading.

macOS Library Validation requires that a dylib loaded by a hardened process either shares the same Apple Developer Team ID as the host binary, or that the host binary carries the com.apple.security.cs.disable-library-validation entitlement.

Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.

This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:

  1. File a Munki issue requesting com.apple.security.cs.disable-library-validation be added to Munki's entitlements (the correct long-term fix).
  2. Use an ad-hoc or unsigned build of the plugin for internal testing only.

catalogs / manifests / icons list not implemented server-side

list("catalogs"), list("manifests"), and list("icons") return empty lists. This means:

  • makecatalogs (separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.
  • iconimporter cannot sync icons via this API.

These require server-side list endpoints that are planned as a follow-up.

Building from source

Requires Xcode 16+ and macOS 12+.

xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath build

Or use the convenience script (also creates a .pkg):

./build_pkg.sh

License

MIT — see LICENSE.

About

Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - EigerCode/uvit-munkiimport-plugin: Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter. · GitHub
Skip to content

Repository files navigation

UVITRepo — Munki repo plugins for UVIT

Repo plugins that let admins push packages directly into UVIT using munkiimport and AutoPkg's MunkiImporter processor. Two plugins, same API and configuration:

  • UVITRepo.plugin (Swift) — for Munki 7's Swift tools (munkiimport etc.), which load dylib plugins from /usr/local/munki/repoplugins/. Requires Munki 7.0 or later.
  • UVITRepo.py (Python) — for AutoPkg's MunkiImporter processor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at /usr/local/munki/munkilib/munkirepo/UVITRepo.py.

Installation

Download the latest UVITRepo-<version>.pkg from the Releases page and install it:

sudo installer -pkg UVITRepo-1.0.0.pkg -target /

This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.

Configuration

1. Get an API token

Log in to the UVIT console and go to My Account → API Tokens → Create Token (/myaccount/tokens). Copy the generated token — it looks like uvit_pat_…. This is your UVIT_TOKEN.

The token identifies you as a user. The server checks your membership for the target you specify (UVIT_TARGET); it does not read a tenant or repo from the token itself.

2. Find your target and repo ID

  • UVIT_TARGET: global if you are a sysadmin pushing to the global scope, or tenant:<id> (e.g. tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.

  • UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.

3. Configure munkiimport

munkiimport --configure

Set the following values when prompted:

SettingValue
repo_urlhttps://<your-console>/api/repo
pluginUVITRepo

Example:

repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo

4. Supply credentials

Option A — environment variables (session-scoped):

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmg

Option B — macOS admin preferences (persistent, recommended for workstations):

sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"

Read them back to verify:

sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimport

Usage

munkiimport /path/to/GoogleChrome.dmg

munkiimport will:

  1. Upload the installer to the UVIT S3 repo (PUT /api/repo/pkgs/…).
  2. Upload the generated pkginfo plist (PUT /api/repo/pkgsinfo/…).
  3. Call makecatalogs — this is a no-op on the UVIT side (catalogs are built dynamically from the database).

The package appears in the UVIT console immediately after a successful push.

AutoPkg

AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a bare CI runner just copy the file into /usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too: MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python munkilib comes with Munki's compatibility component package.

Add these variables to your AutoPkg preferences or pass them on the command line:

<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>

Set the UVIT variables in the environment before running AutoPkg:

export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munki

For unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.

Environment variables / preference keys

Env varPref keyRequiredDescription
UVIT_TOKENuvitTokenYesOpaque API token from My Account → API Tokens (uvit_pat_…).
UVIT_TARGETuvitTargetYesglobal or tenant:<id>. Sent on every request. The server
resolves the tenant from this value and checks membership.
UVIT_REPO_IDuvitRepoIDFor writesNumeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo.

Preferences domain: ch.eigercode.uvit.munkiimport

Endpoint mapping

Munki callUVIT API endpointHeaders
list("pkgsinfo")GET <repo_url>/pkgsinfoAuth + Target
list(<other kind>)returns [] (see Known Limitations below)
get("pkgsinfo/<name>/<ver>.plist")GET <repo_url>/pkgsinfo/<name>/<ver>.plistAuth + Target
get("pkgs/<path>")GET <repo_url>/pkgs/<path> → 307 to S3Auth + Target
put("pkgs/<path>", fromFile:)PUT <repo_url>/pkgs/<path> → 307 to S3Auth + Target + Repo-ID
put("pkgsinfo/<path>", content:)PUT <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgsinfo/<path>")DELETE <repo_url>/pkgsinfo/<path>Auth + Target + Repo-ID
delete("pkgs/<path>")DELETE <repo_url>/pkgs/<path>Auth + Target + Repo-ID
pathFor(_)nil (non-filesystem repo)
makecatalogs (called by Munki tools)POST <repo_url>/makecatalogs → no-op 200Auth + Target

Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>, X-UVIT-Repo-ID: <repoID>.

Presigned pkg uploads (307 redirect)

Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a presigned S3 PUT URL and re-send the file bytes there, without forwarding the Authorization/X-UVIT-* headers (the presigned URL carries its own auth). An older console that predates this (pre-uvit-console#575) never sends a redirect on that PUT, so both plugins keep working against it unchanged.

Known limitations

dylib code-signing / Library Validation (Swift plugin only; must verify on a real Mac)

This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg runs are unaffected — they use the Python plugin, which involves no dylib loading.

macOS Library Validation requires that a dylib loaded by a hardened process either shares the same Apple Developer Team ID as the host binary, or that the host binary carries the com.apple.security.cs.disable-library-validation entitlement.

Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.

This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:

  1. File a Munki issue requesting com.apple.security.cs.disable-library-validation be added to Munki's entitlements (the correct long-term fix).
  2. Use an ad-hoc or unsigned build of the plugin for internal testing only.

catalogs / manifests / icons list not implemented server-side

list("catalogs"), list("manifests"), and list("icons") return empty lists. This means:

  • makecatalogs (separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.
  • iconimporter cannot sync icons via this API.

These require server-side list endpoints that are planned as a follow-up.

Building from source

Requires Xcode 16+ and macOS 12+.

xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath build

Or use the convenience script (also creates a .pkg):

./build_pkg.sh

License

MIT — see LICENSE.

About

Munki 7 (Swift) repo plugin that pushes packages into UVIT via the /api/repo ingest API. Also usable by AutoPkg's MunkiImporter.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages