add ability to authenticate to registry

This commit is contained in:
James Brumond 2023-08-18 21:25:38 -07:00
parent 4d4170efa7
commit c5b78bf494
Signed by: james
GPG Key ID: E8F2FC44BAA3357A
4 changed files with 142 additions and 12 deletions

View File

@ -16,3 +16,9 @@ inputs:
new-tags:
description: Newline delimited list of new tags to apply to the image
required: true
username:
description: Username to use to authenticate to the container registry
required: false
password:
description: Password to use to authenticate to the container registry
required: false

125
index.js
View File

@ -2,6 +2,8 @@
const core = require('@actions/core');
const http = require('http');
const https = require('https');
const querystring = require('querystring');
const form_urlencoded = require('form-urlencoded');
const req_timeout_ms = 30_000;
const manifest_media_type = 'application/vnd.docker.distribution.manifest.v2+json';
@ -15,6 +17,9 @@ async function main() {
image: core.getInput('image'),
old_tag: core.getInput('old-tag'),
new_tags: core.getInput('new-tags').trim().split('\n'),
username: core.getInput('username'),
password: core.getInput('password'),
token: null,
};
console.log('Tagging %s/%s:%s with new tags', input.registry, input.image, input.old_tag, input.new_tags);
@ -34,11 +39,20 @@ async function main() {
}
}
async function get_manifest(url_str) {
const { status, headers, body } = await http_req('GET', url_str, {
async function get_manifest(url_str, input) {
let { status, headers, body } = await http_req('GET', url_str, {
accept: manifest_media_type,
});
if (status === 401 && headers['www-authenticate']) {
await get_token(headers['www-authenticate'], input);
({ status, headers, body } = await http_req('GET', url_str, {
accept: manifest_media_type,
authorization: `Bearer ${token}`,
}));
}
if (status !== 200) {
console.error('get manifest response', { status, headers, body });
throw new Error('failed to fetch existing manifest');
@ -48,9 +62,15 @@ async function get_manifest(url_str) {
}
async function put_manifest(url_str, manifest) {
const { status, headers, body } = await http_req('PUT', url_str, {
const req_headers = {
'content-type': manifest_media_type,
}, manifest);
};
if (token) {
req_headers.authorization = `Bearer ${token}`;
}
const { status, headers, body } = await http_req('PUT', url_str, req_headers, manifest);
if (status >= 400) {
console.error('get manifest response', { status, headers, body });
@ -58,6 +78,40 @@ async function put_manifest(url_str, manifest) {
}
}
async function get_token(www_authenticate, input) {
const params = parse_www_authenticate(www_authenticate);
if (! input.username || ! input.password) {
throw new Error('registry requested authentication, but not credentials were provided');
}
const query_params = {
service: params.service,
scope: params.scope,
grant_type: 'password',
username: input.username,
password: input.password,
};
const { status, headers, body } = await http_req('POST', params.realm, {
'content-type': 'application/x-www-form-urlencoded',
}, form_urlencoded(query_params));
if (status !== 200) {
console.error('get token response', { status, headers, body });
throw new Error('error response from get token request');
}
try {
const { token } = JSON.parse(body);
input.token = token;
}
catch (error) {
throw new Error('invalid response from get token request');
}
}
async function http_req(method, url_str, headers, body) {
const url = new URL(url_str);
const make_request
@ -121,3 +175,66 @@ async function http_req(method, url_str, headers, body) {
req.end();
});
}
function parse_www_authenticate(www_authenticate) {
if (! www_authenticate.startsWith('Bearer ')) {
throw new Error('invalid www-authenticate header (no "Bearer " prefix)');
}
let remaining = www_authenticate.slice(7);
const parameters = Object.create(null);
while (remaining) {
const eq_index = remaining.indexOf('=');
if (eq_index < 0) {
parameters[remaining] = '';
break;
}
const key = remaining.slice(0, eq_index);
remaining = remaining.slice(eq_index + 1);
if (! remaining) {
parameters[key] = '';
break;
}
if (remaining[0] === '"') {
const close_index = remaining.slice(1).indexOf('"');
if (close_index < 0) {
throw new Error('invalid www-authenticate header (unclosed quotes)');
}
parameters[key] = remaining.slice(1, close_index);
remaining = remaining.slice(close_index + 1);
if (remaining && remaining[0] !== ',') {
throw new Error('invalid www-authenticate header (expected comma after closing quote)');
}
remaining = remaining.slice(1);
}
else {
const comma_index = remaining.indexOf(',');
if (comma_index < 0) {
parameters[key] = remaining;
break;
}
parameters[key] = remaining.slice(1, comma_index);
remaining = remaining.slice(comma_index + 1);
}
}
const { realm, service, scope } = parameters;
if (! realm || ! service || ! scope) {
throw new Error('invalid www-authenticate header (missing "realm", "service", or "scope")');
}
return parameters;
}

8
package-lock.json generated
View File

@ -5,7 +5,8 @@
"packages": {
"": {
"dependencies": {
"@actions/core": "^1.10.0"
"@actions/core": "^1.10.0",
"form-urlencoded": "^6.1.0"
},
"devDependencies": {
"@vercel/ncc": "^0.36.1",
@ -579,6 +580,11 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
"node_modules/form-urlencoded": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-6.1.0.tgz",
"integrity": "sha512-lc1Qd9nnEewXKoiPjIA1n38M5STbyY6krgoegsg7SsAt2b98HZKe25KaJvKFBwQaOcmh8FP7JbXVC7gocZw+XQ=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",

View File

@ -1,15 +1,16 @@
{
"private": true,
"scripts": {
"lint": "eslint .",
"prepare": "ncc build index.js -o dist --source-map --license licenses.txt",
"all": "npm run lint && npm run prepare"
"lint": "eslint .",
"prepare": "ncc build index.js -o dist --source-map --license licenses.txt",
"all": "npm run lint && npm run prepare"
},
"dependencies": {
"@actions/core": "^1.10.0"
"@actions/core": "^1.10.0",
"form-urlencoded": "^6.1.0"
},
"devDependencies": {
"@vercel/ncc": "^0.36.1",
"eslint": "^8.37.0"
"@vercel/ncc": "^0.36.1",
"eslint": "^8.37.0"
}
}
}