mirror of
https://github.com/okxlin/appstore.git
synced 2026-09-28 08:01:07 +00:00
Add a Docker Hub credential preflight
Validate the configured Docker Hub credentials against the token service and a public manifest before starting a full Renovate scan. Provide a small manual workflow so maintainers can diagnose authentication without consuming a full scan.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
const AUTH_URL =
|
||||
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull";
|
||||
const MANIFEST_URL =
|
||||
"https://registry-1.docker.io/v2/library/alpine/manifests/latest";
|
||||
|
||||
export async function checkDockerHubCredentials({
|
||||
username,
|
||||
password,
|
||||
fetchImpl = fetch,
|
||||
}) {
|
||||
if (!username || !password) {
|
||||
throw new Error(
|
||||
"Configure DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets.",
|
||||
);
|
||||
}
|
||||
|
||||
const basicAuth = Buffer.from(`${username}:${password}`).toString("base64");
|
||||
const authResponse = await fetchImpl(AUTH_URL, {
|
||||
headers: { authorization: `Basic ${basicAuth}` },
|
||||
});
|
||||
if (!authResponse.ok) {
|
||||
throw new Error(
|
||||
`Docker Hub rejected the configured credentials (HTTP ${authResponse.status}).`,
|
||||
);
|
||||
}
|
||||
|
||||
const authPayload = await authResponse.json();
|
||||
const bearer = authPayload.token ?? authPayload.access_token;
|
||||
if (!bearer) {
|
||||
throw new Error("Docker Hub did not return a registry bearer token.");
|
||||
}
|
||||
|
||||
const manifestResponse = await fetchImpl(MANIFEST_URL, {
|
||||
headers: {
|
||||
accept:
|
||||
"application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json",
|
||||
authorization: `Bearer ${bearer}`,
|
||||
},
|
||||
});
|
||||
if (!manifestResponse.ok) {
|
||||
throw new Error(
|
||||
`Docker Hub authenticated manifest lookup failed (HTTP ${manifestResponse.status}).`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
limit:
|
||||
manifestResponse.headers.get("ratelimit-limit") ??
|
||||
manifestResponse.headers.get("x-ratelimit-limit") ??
|
||||
"not reported",
|
||||
remaining:
|
||||
manifestResponse.headers.get("ratelimit-remaining") ??
|
||||
manifestResponse.headers.get("x-ratelimit-remaining") ??
|
||||
"not reported",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
||||
) {
|
||||
try {
|
||||
const result = await checkDockerHubCredentials({
|
||||
username: process.env.RENOVATE_DOCKERHUB_USERNAME,
|
||||
password: process.env.RENOVATE_DOCKERHUB_TOKEN,
|
||||
});
|
||||
console.log(
|
||||
`Docker Hub credential preflight passed (limit=${result.limit}, remaining=${result.remaining}).`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
import { resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -211,6 +211,12 @@ class RenovateAppVersionTests(unittest.TestCase):
|
||||
|
||||
def test_self_hosted_renovate_requires_docker_hub_credentials(self):
|
||||
workflow = (REPO_ROOT / ".github" / "workflows" / "renovate.yml").read_text(encoding="utf-8")
|
||||
preflight = (
|
||||
REPO_ROOT / ".github" / "workflows" / "renovate-dockerhub-preflight.yml"
|
||||
).read_text(encoding="utf-8")
|
||||
checker = (
|
||||
REPO_ROOT / ".github" / "scripts" / "check_dockerhub_credentials.mjs"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("configurationFile: .github/renovate-global.js", workflow)
|
||||
self.assertIn(
|
||||
@@ -220,7 +226,53 @@ class RenovateAppVersionTests(unittest.TestCase):
|
||||
self.assertIn("RENOVATE_DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}", workflow)
|
||||
self.assertIn("RENOVATE_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}", workflow)
|
||||
self.assertIn("name: Check Docker Hub credentials", workflow)
|
||||
self.assertIn("Configure DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets", workflow)
|
||||
self.assertIn("node .github/scripts/check_dockerhub_credentials.mjs", workflow)
|
||||
self.assertIn("workflow_dispatch:", preflight)
|
||||
self.assertIn("node .github/scripts/check_dockerhub_credentials.mjs", preflight)
|
||||
self.assertIn("https://auth.docker.io/token", checker)
|
||||
self.assertIn("https://registry-1.docker.io/v2/library/alpine/manifests/latest", checker)
|
||||
self.assertNotIn("console.log(token", checker)
|
||||
self.assertNotIn("console.log(password", checker)
|
||||
|
||||
def test_docker_hub_preflight_validates_auth_and_manifest_without_logging_token(self):
|
||||
checker = REPO_ROOT / ".github" / "scripts" / "check_dockerhub_credentials.mjs"
|
||||
expression = f"""
|
||||
import {{ checkDockerHubCredentials }} from {json.dumps(checker.as_uri())};
|
||||
const requests = [];
|
||||
const responses = [
|
||||
{{ ok: true, status: 200, json: async () => ({{ token: 'bearer-secret' }}) }},
|
||||
{{
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Headers({{
|
||||
'ratelimit-limit': '200;w=21600',
|
||||
'ratelimit-remaining': '199;w=21600',
|
||||
}}),
|
||||
}},
|
||||
];
|
||||
const result = await checkDockerHubCredentials({{
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
fetchImpl: async (url, options) => {{
|
||||
requests.push({{ url, authorization: options.headers.authorization }});
|
||||
return responses.shift();
|
||||
}},
|
||||
}});
|
||||
console.log(JSON.stringify({{ result, requests }}));
|
||||
"""
|
||||
|
||||
result = subprocess.run(
|
||||
["node", "--input-type=module", "-e", expression],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
self.assertEqual(0, result.returncode)
|
||||
self.assertEqual("199;w=21600", payload["result"]["remaining"])
|
||||
self.assertTrue(payload["requests"][0]["authorization"].startswith("Basic "))
|
||||
self.assertEqual("Bearer bearer-secret", payload["requests"][1]["authorization"])
|
||||
|
||||
def test_self_hosted_renovate_uses_a_pinned_persistent_cache(self):
|
||||
workflow = (REPO_ROOT / ".github" / "workflows" / "renovate.yml").read_text(
|
||||
|
||||
Reference in New Issue
Block a user