275 lines
7.0 KiB
JavaScript
275 lines
7.0 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');
|
|
}
|
|
|
|
console.log('get manifest response', { status, headers, body });
|
|
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;
|
|
// }
|