add ability to authenticate to registry
This commit is contained in:
parent
4d4170efa7
commit
c5b78bf494
@ -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
125
index.js
@ -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
8
package-lock.json
generated
@ -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",
|
||||
|
15
package.json
15
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user