tag-manifest/index.js

274 lines
6.9 KiB
JavaScript

const core = require('@actions/core');
const http = require('http');
const https = require('https');
const querystring = require('querystring');
const req_timeout_ms = 30_000;
const manifest_media_type = 'application/vnd.docker.distribution.manifest.v2+json';
main();
async function main() {
try {
const input = {
registry: core.getInput('registry'),
registry_url: null,
insecure: core.getInput('insecure') === 'true',
username: core.getInput('username'),
password: core.getInput('password'),
manifest: parse_manifest_tag(core.getInput('manifest')),
tags: parse_tags(core.getInput('tags').trim()),
token: null,
};
input.registry_url = `http${input.insecure ? '' : 's'}://${input.registry}`;
console.log('Tagging %s/%s:%s with new tags', input.registry, input.manifest.image, input.manifest.tag, input.tags);
const manifest = await get_manifest(input);
const promises = input.tags.map((tag) => {
return put_manifest(tag, manifest, input);
});
await Promise.all(promises);
}
catch (error) {
console.error(error);
core.setFailed(error.message);
}
}
function parse_manifest_tag(full_tag) {
const colon_index = full_tag.lastIndexOf(':');
const image = full_tag.slice(0, colon_index);
const tag = full_tag.slice(colon_index + 1);
return { image, tag };
}
function parse_tags(tags) {
if (tags.indexOf('\n') < 0) {
return tags.split(',').map((tag) => tag.trim());
}
return tags.split('\n').map((tag) => tag.trim());
}
async function get_manifest(input) {
const path = `/v2/${input.manifest.image}/manifests/${input.manifest.tag}`;
const req_headers = {
accept: manifest_media_type,
};
http_basic_auth(req_headers, input);
let { status, headers, body } = await http_req('GET', input.registry_url + path, req_headers);
// if (status === 401 && headers['www-authenticate']) {
// await get_token(headers['www-authenticate'], input);
// ({ status, headers, body } = await http_req('GET', input.registry_url + path, {
// accept: manifest_media_type,
// authorization: `Bearer ${input.token}`,
// }));
// }
if (status !== 200) {
console.error('get manifest response', { status, headers, body });
throw new Error('failed to fetch existing manifest');
}
return body;
}
async function put_manifest(new_tag, manifest, input) {
const path = `/v2/${input.manifest.image}/manifests/${new_tag}`;
const content = Buffer.from(manifest, 'utf8');
const req_headers = {
'content-type': manifest_media_type,
'content-length': content.byteLength,
};
http_basic_auth(req_headers, input);
// if (input.token) {
// req_headers.authorization = `Bearer ${input.token}`;
// }
const { status, headers, body } = await http_req('PUT', input.registry_url + path, req_headers, content);
if (status >= 400) {
console.error('put manifest response', { status, headers, body });
throw new Error('failed to put manifest');
}
}
function http_basic_auth(headers, input) {
if (input.username) {
const credentials = Buffer.from(`${input.username}:${input.password}`, 'utf8').toString('base64');
headers.authorization = `Basic ${credentials}`;
}
}
// 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: `repository:${input.manifest.image}:pull,push`,
// grant_type: 'password',
// username: input.username,
// password: input.password,
// };
// const { status, headers, body } = await http_req('GET', params.realm + '?' + querystring.stringify(query_params), {
// 'content-type': 'application/x-www-form-urlencoded',
// });
// 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
= url.protocol === 'https:' ? https.request
: url.protocol === 'http:' ? http.request
: null;
if (! make_request) {
throw new Error('registry URL protocol not http(s)');
}
if (url.username || url.password) {
throw new Error('urls containing user credentials not allowed');
}
const result = {
url,
status: null,
body: null,
headers: null,
};
const path = url.pathname + (url.search || '');
const port = url.port ? parseInt(url.port, 10) : (url.protocol === 'https:' ? 443 : 80);
return new Promise((resolve, reject) => {
const req = make_request({
method: method,
protocol: url.protocol,
hostname: url.hostname,
port: port,
path: path,
headers: headers
}, (res) => {
result.status = res.statusCode;
result.body = '';
res.on('data', (chunk) => {
result.body += chunk;
});
res.on('end', () => {
result.headers = res.headers;
resolve(result);
});
});
req.setTimeout(req_timeout_ms, () => {
req.destroy();
reject(new Error('request timeout'));
});
req.on('error', (error) => {
reject(error);
});
if (body) {
req.write(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 + 1);
// remaining = remaining.slice(close_index + 2);
// 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;
// }