feat: add Puppet Forge remote type (#44)
## Summary - Adds \`package: puppet\` for proxying Puppet Forge (forgeapi.puppet.com) - \`remote/puppet.py\` rewrites JSON responses: absolute forge URLs → proxy URLs, and relative \`/v3/files/\` \`file_uri\` paths → absolute proxy URLs. g10k uses Go's \`url.ResolveReference\`, so an absolute \`file_uri\` overrides the base URL entirely — tarballs are fetched directly from the proxy without a second hop - Built-in mutable patterns: \`^v3/modules/\` and \`^v3/releases\` (module metadata); tarballs at \`v3/files/\` are configured as immutable via \`immutable_patterns\` - 9 new tests covering mutable detection, URL rewriting (relative \`file_uri\` and absolute forge URLs), content-type, tarball pass-through, and pattern blocking ## Client configuration **g10k config file** (\`forge_base_url\` at root level): \`\`\`yaml cachedir: /tmp/g10k forge_base_url: https://artifacts.example.com/api/v1/remote/puppet-forge sources: control: remote: git@git.example.com:puppet/control.git basedir: /etc/puppetlabs/code/environments \`\`\` **Puppetfile** (\`forge.baseUrl\` directive, works with \`-puppetfile\` mode): \`\`\`ruby forge.baseUrl https://artifacts.example.com/api/v1/remote/puppet-forge mod 'puppetlabs-stdlib', '9.7.0' \`\`\` ## Test plan - [x] 331 unit tests pass (\`make test\`) - [x] End-to-end: g10k 0.9.10 on AlmaLinux 9 via \`forge_base_url\` — stdlib 9.7.0, inifile 6.2.0, concat 9.1.0 installed; proxy logs confirm cache MISS → fetch → ADD for metadata and tarballs - [x] End-to-end: \`forge.baseUrl\` Puppetfile directive with \`-puppetfile\` mode — same result Reviewed-on: #44
This commit was merged in pull request #44.
This commit is contained in:
@@ -112,6 +112,12 @@ TEST_REMOTES = {
|
||||
"immutable_patterns": [r"\.tgz$"],
|
||||
"cache": {"immutable_ttl": 0, "mutable_ttl": 1800},
|
||||
},
|
||||
"puppet-test": {
|
||||
"base_url": "https://forgeapi.puppet.com",
|
||||
"package": "puppet",
|
||||
"immutable_patterns": [r"^v3/files/.*\.tar\.gz$"],
|
||||
"cache": {"immutable_ttl": 0, "mutable_ttl": 600},
|
||||
},
|
||||
},
|
||||
"locals": {
|
||||
"local-test": {
|
||||
|
||||
@@ -1117,6 +1117,123 @@ class TestHelmRemote:
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Puppet Forge remote /api/v1/remote/puppet-test/...
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPuppetRemote:
|
||||
def test_module_metadata_is_mutable(self, client, patched_deps):
|
||||
"""v3/modules/ paths are detected as mutable (package-type default)."""
|
||||
deps = patched_deps
|
||||
deps["storage"].exists.return_value = True
|
||||
deps["storage"].download_object.return_value = b'{"current_release":{"file_uri":"/v3/files/puppetlabs-stdlib-9.7.0.tar.gz"}}'
|
||||
deps["cache"].is_mutable_file.return_value = True
|
||||
deps["cache"].is_index_valid.return_value = True
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/modules/puppetlabs-stdlib")
|
||||
assert response.status_code == 200
|
||||
deps["cache"].mark_index_cached.assert_not_called()
|
||||
|
||||
def test_releases_path_is_mutable(self, client, patched_deps):
|
||||
"""v3/releases paths are detected as mutable (package-type default)."""
|
||||
deps = patched_deps
|
||||
deps["storage"].exists.return_value = True
|
||||
deps["storage"].download_object.return_value = b'{"file_uri":"/v3/files/puppetlabs-stdlib-9.7.0.tar.gz"}'
|
||||
deps["cache"].is_mutable_file.return_value = True
|
||||
deps["cache"].is_index_valid.return_value = True
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/releases/puppetlabs-stdlib-9.7.0")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_relative_file_uri_rewritten_to_absolute_proxy_url(self, client, patched_deps):
|
||||
"""Relative /v3/files/ paths in JSON responses are rewritten to absolute proxy URLs."""
|
||||
deps = patched_deps
|
||||
meta = b'{"current_release":{"file_uri":"/v3/files/puppetlabs-stdlib-9.7.0.tar.gz","version":"9.7.0"}}'
|
||||
deps["storage"].exists.return_value = True
|
||||
deps["storage"].download_object.return_value = meta
|
||||
deps["cache"].is_mutable_file.return_value = True
|
||||
deps["cache"].is_index_valid.return_value = True
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/modules/puppetlabs-stdlib")
|
||||
assert response.status_code == 200
|
||||
assert b'"/v3/files/' not in response.content
|
||||
assert b"/api/v1/remote/puppet-test/v3/files/puppetlabs-stdlib-9.7.0.tar.gz" in response.content
|
||||
|
||||
def test_absolute_forge_url_rewritten_to_proxy(self, client, patched_deps):
|
||||
"""Absolute forgeapi.puppet.com URLs in JSON are rewritten to the proxy URL."""
|
||||
deps = patched_deps
|
||||
meta = b'{"uri":"https://forgeapi.puppet.com/v3/modules/puppetlabs-stdlib"}'
|
||||
deps["storage"].exists.return_value = True
|
||||
deps["storage"].download_object.return_value = meta
|
||||
deps["cache"].is_mutable_file.return_value = True
|
||||
deps["cache"].is_index_valid.return_value = True
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/modules/puppetlabs-stdlib")
|
||||
assert response.status_code == 200
|
||||
assert b"forgeapi.puppet.com" not in response.content
|
||||
assert b"/api/v1/remote/puppet-test" in response.content
|
||||
|
||||
def test_metadata_content_type_is_json(self, client, patched_deps):
|
||||
deps = patched_deps
|
||||
deps["storage"].exists.return_value = True
|
||||
deps["storage"].download_object.return_value = b'{"current_release":{}}'
|
||||
deps["cache"].is_mutable_file.return_value = True
|
||||
deps["cache"].is_index_valid.return_value = True
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/modules/puppetlabs-concat")
|
||||
assert response.status_code == 200
|
||||
assert "application/json" in response.headers["content-type"]
|
||||
|
||||
def test_tarball_served_without_rewriting(self, client, patched_deps):
|
||||
"""Module tarballs (v3/files/*.tar.gz) are served as binary without URL rewriting."""
|
||||
deps = patched_deps
|
||||
deps["storage"].exists.return_value = True
|
||||
deps["storage"].download_object.return_value = b"\x1f\x8b tarball bytes"
|
||||
deps["cache"].is_mutable_file.return_value = False
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/files/puppetlabs-stdlib-9.7.0.tar.gz")
|
||||
assert response.status_code == 200
|
||||
assert "application/gzip" in response.headers["content-type"]
|
||||
assert response.headers["X-Artifact-Source"] == "cache"
|
||||
|
||||
def test_tarball_not_blocked_by_immutable_pattern(self, client, patched_deps):
|
||||
"""v3/files/*.tar.gz matches the configured immutable_patterns and is allowed."""
|
||||
deps = patched_deps
|
||||
deps["storage"].exists.return_value = True
|
||||
deps["storage"].download_object.return_value = b"\x1f\x8b tarball bytes"
|
||||
deps["cache"].is_mutable_file.return_value = False
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/files/puppetlabs-inifile-6.2.0.tar.gz")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_unknown_path_blocked(self, client, patched_deps):
|
||||
"""Paths outside v3/modules, v3/releases, and v3/files are blocked."""
|
||||
deps = patched_deps
|
||||
deps["cache"].is_mutable_file.return_value = False
|
||||
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/users/puppetlabs")
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_metadata_cache_miss_fetches_upstream(self, client, patched_deps):
|
||||
deps = patched_deps
|
||||
meta = b'{"current_release":{"file_uri":"/v3/files/puppetlabs-stdlib-9.7.0.tar.gz"}}'
|
||||
deps["storage"].exists.return_value = False
|
||||
deps["storage"].download_object.return_value = meta
|
||||
deps["cache"].is_mutable_file.return_value = True
|
||||
|
||||
with patch(
|
||||
"artifactapi.artifact.proxy.cache_single_artifact",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"status": "cached"},
|
||||
) as mock_fetch:
|
||||
response = client.get("/api/v1/remote/puppet-test/v3/modules/puppetlabs-stdlib")
|
||||
|
||||
mock_fetch.assert_called_once()
|
||||
assert response.status_code == 200
|
||||
assert b'"/v3/files/' not in response.content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Quarantine (quarantine-test remote: quarantine_new=True, quarantine_days=3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user