mirror of
https://github.com/docker/build-push-action.git
synced 2025-03-30 19:50:07 +02:00
Sanitize tags
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
parent
8891861577
commit
92991ec63d
4 changed files with 616 additions and 2 deletions
186
__tests__/image-reference.test.ts
Normal file
186
__tests__/image-reference.test.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import {ImageReference} from '../src/image-reference';
|
||||||
|
|
||||||
|
describe('fromString', () => {
|
||||||
|
// prettier-ignore
|
||||||
|
test.each([
|
||||||
|
[
|
||||||
|
'foo_com',
|
||||||
|
{
|
||||||
|
path: 'foo_com'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo.com:tag',
|
||||||
|
{
|
||||||
|
path: 'foo.com',
|
||||||
|
tag: 'tag'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo.com:5000',
|
||||||
|
{
|
||||||
|
path: 'foo.com',
|
||||||
|
tag: '5000'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo.com/repo:tag',
|
||||||
|
{
|
||||||
|
domain: 'foo.com',
|
||||||
|
path: 'repo',
|
||||||
|
tag: 'tag'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo.com:5000/repo',
|
||||||
|
{
|
||||||
|
domain: 'foo.com:5000',
|
||||||
|
path: 'repo'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo.com:5000/repo:tag',
|
||||||
|
{
|
||||||
|
domain: 'foo.com:5000',
|
||||||
|
path: 'repo',
|
||||||
|
tag: 'tag'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
{
|
||||||
|
domain: 'foo:5000',
|
||||||
|
path: 'repo',
|
||||||
|
digest: 'sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
{
|
||||||
|
domain: 'foo:5000',
|
||||||
|
path: 'repo',
|
||||||
|
tag: 'tag',
|
||||||
|
digest: 'sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'foo:5000/repo',
|
||||||
|
{
|
||||||
|
domain: 'foo:5000',
|
||||||
|
path: 'repo'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
':justtag',
|
||||||
|
{},
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'b.gcr.io/foo.example.com/my-app:foo.example.com',
|
||||||
|
{
|
||||||
|
domain: 'b.gcr.io',
|
||||||
|
path: 'foo.example.com/my-app',
|
||||||
|
tag: 'foo.example.com',
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'docker.io/library/ubuntu:18.04@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
{
|
||||||
|
domain: 'docker.io',
|
||||||
|
path: 'library/ubuntu',
|
||||||
|
tag: '18.04',
|
||||||
|
digest: 'sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ghactionstest/ghactionstest',
|
||||||
|
{
|
||||||
|
domain: 'ghactionstest',
|
||||||
|
path: 'ghactionstest'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ghcr.io/docker-ghactiontest/test',
|
||||||
|
{
|
||||||
|
domain: 'ghcr.io',
|
||||||
|
path: 'docker-ghactiontest/test'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'registry.gitlab.com/test1716/test',
|
||||||
|
{
|
||||||
|
domain: 'registry.gitlab.com',
|
||||||
|
path: 'test1716/test'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'175142243308.dkr.ecr.us-east-2.amazonaws.com/sandbox/test-docker-action',
|
||||||
|
{
|
||||||
|
domain: '175142243308.dkr.ecr.us-east-2.amazonaws.com',
|
||||||
|
path: 'sandbox/test-docker-action'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'public.ecr.aws/q3b5f1u4/test-docker-action',
|
||||||
|
{
|
||||||
|
domain: 'public.ecr.aws',
|
||||||
|
path: 'q3b5f1u4/test-docker-action'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'us-east4-docker.pkg.dev/sandbox-298914/docker-official-github-actions/test-docker-action',
|
||||||
|
{
|
||||||
|
domain: 'us-east4-docker.pkg.dev',
|
||||||
|
path: 'sandbox-298914/docker-official-github-actions/test-docker-action'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gcr.io/sandbox-298914/test-docker-action',
|
||||||
|
{
|
||||||
|
domain: 'gcr.io',
|
||||||
|
path: 'sandbox-298914/test-docker-action'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ghcr.io/KTH-Library/kontarion:latest',
|
||||||
|
{
|
||||||
|
domain: 'ghcr.io',
|
||||||
|
path: 'kth-library/kontarion',
|
||||||
|
tag: 'latest'
|
||||||
|
},
|
||||||
|
false
|
||||||
|
],
|
||||||
|
])(
|
||||||
|
'given %p',
|
||||||
|
async (input, expected, invalid) => {
|
||||||
|
try {
|
||||||
|
const ir = ImageReference.fromString(input);
|
||||||
|
console.log(ir);
|
||||||
|
expect(ir).toEqual(expected);
|
||||||
|
} catch (err) {
|
||||||
|
if (!invalid) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
expect(true).toBe(invalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
191
dist/index.js
generated
vendored
191
dist/index.js
generated
vendored
|
@ -3498,6 +3498,194 @@ const minVersion = (range, loose) => {
|
||||||
module.exports = minVersion
|
module.exports = minVersion
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 184:
|
||||||
|
/***/ (function(__unusedmodule, exports) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Grammar
|
||||||
|
//
|
||||||
|
// reference := name [ ":" tag ] [ "@" digest ]
|
||||||
|
// name := [domain '/'] path-component ['/' path-component]*
|
||||||
|
// domain := domain-component ['.' domain-component]* [':' port-number]
|
||||||
|
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
|
||||||
|
// port-number := /[0-9]+/
|
||||||
|
// path-component := alpha-numeric [separator alpha-numeric]*
|
||||||
|
// alpha-numeric := /[a-z0-9]+/
|
||||||
|
// separator := /[_.]|__|[-]*/
|
||||||
|
//
|
||||||
|
// tag := /[\w][\w.-]{0,127}/
|
||||||
|
//
|
||||||
|
// digest := digest-algorithm ":" digest-hex
|
||||||
|
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
|
||||||
|
// digest-algorithm-separator := /[+.-_]/
|
||||||
|
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
|
||||||
|
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
|
||||||
|
//
|
||||||
|
// identifier := /[a-f0-9]{64}/
|
||||||
|
// short-identifier := /[a-f0-9]{6,64}/
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/distribution/distribution/blob/master/reference/reference.go
|
||||||
|
// Ref: https://github.com/distribution/distribution/blob/master/reference/regexp.go
|
||||||
|
// Ref: https://github.com/moby/moby/blob/master/image/spec/v1.2.md
|
||||||
|
// Ref: https://github.com/jkcfg/kubernetes/blob/master/src/image-reference.ts
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ImageReference = void 0;
|
||||||
|
// nameMaxLength is the maximum total number of characters in a repository name.
|
||||||
|
const nameMaxLength = 255;
|
||||||
|
function match(s) {
|
||||||
|
if (s instanceof RegExp) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return new RegExp(s);
|
||||||
|
}
|
||||||
|
function quoteMeta(s) {
|
||||||
|
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||||
|
}
|
||||||
|
// literal compiles s into a literal regular expression, escaping any regexp
|
||||||
|
// reserved characters.
|
||||||
|
function literal(s) {
|
||||||
|
return match(quoteMeta(s));
|
||||||
|
}
|
||||||
|
// expression defines a full expression, where each regular expression must
|
||||||
|
// follow the previous.
|
||||||
|
function expression(...res) {
|
||||||
|
let s = '';
|
||||||
|
for (const re of res) {
|
||||||
|
s += re.source;
|
||||||
|
}
|
||||||
|
return match(s);
|
||||||
|
}
|
||||||
|
// optional wraps the expression in a non-capturing group and makes the
|
||||||
|
// production optional.
|
||||||
|
function optional(...res) {
|
||||||
|
return match(group(expression(...res)).source + '?');
|
||||||
|
}
|
||||||
|
// repeated wraps the regexp in a non-capturing group to get one or more
|
||||||
|
// matches.
|
||||||
|
function repeated(...res) {
|
||||||
|
return match(group(expression(...res)).source + '+');
|
||||||
|
}
|
||||||
|
// capture wraps the expression in a capturing group.
|
||||||
|
function capture(...res) {
|
||||||
|
return match(`(` + expression(...res).source + `)`);
|
||||||
|
}
|
||||||
|
// anchored anchors the regular expression by adding start and end delimiters.
|
||||||
|
function anchored(...res) {
|
||||||
|
return match(`^` + expression(...res).source + `$`);
|
||||||
|
}
|
||||||
|
// group wraps the regexp in a non-capturing group.
|
||||||
|
function group(...res) {
|
||||||
|
return match(`(?:${expression(...res).source})`);
|
||||||
|
}
|
||||||
|
// alphaNumericRegexp defines the alpha numeric atom, typically a component of
|
||||||
|
// names. Can contain upper case characters compared to the default API to
|
||||||
|
// fix https://github.com/docker/build-push-action/issues/237#issue-748654527
|
||||||
|
const alphaNumericRegexp = match(/[a-zA-Z0-9]+/);
|
||||||
|
// separatorRegexp defines the separators allowed to be embedded in name components.
|
||||||
|
// This allow one period, one or two underscore and multiple dashes.
|
||||||
|
const separatorRegexp = match(/(?:[._]|__|[-]*)/);
|
||||||
|
// nameComponentRegexp restricts registry path component names to start with at
|
||||||
|
// least one letter or number, with following parts able to be separated by one
|
||||||
|
// period, one or two underscore and multiple dashes.
|
||||||
|
const nameComponentRegexp = expression(alphaNumericRegexp, optional(repeated(separatorRegexp, alphaNumericRegexp)));
|
||||||
|
// domainComponentRegexp restricts the registry domain component of a
|
||||||
|
// repository name to start with a component as defined by DomainRegexp
|
||||||
|
// and followed by an optional port.
|
||||||
|
const domainComponentRegexp = match(/(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/);
|
||||||
|
// DomainRegexp defines the structure of potential domain components
|
||||||
|
// that may be part of image names. This is purposely a subset of what is
|
||||||
|
// allowed by DNS to ensure backwards compatibility with Docker image
|
||||||
|
// names.
|
||||||
|
const DomainRegexp = expression(domainComponentRegexp, optional(repeated(literal(`.`), domainComponentRegexp)), optional(literal(`:`), match(/[0-9]+/)));
|
||||||
|
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
||||||
|
const TagRegexp = match(/[\w][\w.-]{0,127}/);
|
||||||
|
// anchoredTagRegexp matches valid tag names, anchored at the start and
|
||||||
|
// end of the matched string.
|
||||||
|
const anchoredTagRegexp = anchored(TagRegexp);
|
||||||
|
// DigestRegexp matches valid digests.
|
||||||
|
const DigestRegexp = match(/[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9a-fA-F]{32,}/);
|
||||||
|
// anchoredDigestRegexp matches valid digests, anchored at the start and
|
||||||
|
// end of the matched string.
|
||||||
|
const anchoredDigestRegexp = anchored(DigestRegexp);
|
||||||
|
// NameRegexp is the format for the name component of references. The
|
||||||
|
// regexp has capturing groups for the domain and name part omitting
|
||||||
|
// the separating forward slash from either.
|
||||||
|
const NameRegexp = expression(optional(DomainRegexp, literal(`/`)), nameComponentRegexp, optional(repeated(literal(`/`), nameComponentRegexp)));
|
||||||
|
// anchoredNameRegexp is used to parse a name value, capturing the
|
||||||
|
// domain and trailing components.
|
||||||
|
const anchoredNameRegexp = anchored(optional(capture(DomainRegexp), literal(`/`)), capture(nameComponentRegexp, optional(repeated(literal(`/`), nameComponentRegexp))));
|
||||||
|
// ReferenceRegexp is the full supported format of a reference. The regexp
|
||||||
|
// is anchored and has capturing groups for name, tag, and digest
|
||||||
|
// components.
|
||||||
|
const ReferenceRegexp = anchored(capture(NameRegexp), optional(literal(':'), capture(TagRegexp)), optional(literal('@'), capture(DigestRegexp)));
|
||||||
|
// IdentifierRegexp is the format for string identifier used as a
|
||||||
|
// content addressable identifier using sha256. These identifiers
|
||||||
|
// are like digests without the algorithm, since sha256 is used.
|
||||||
|
const IdentifierRegexp = match(/([a-f0-9]{64})/);
|
||||||
|
// ShortIdentifierRegexp is the format used to represent a prefix
|
||||||
|
// of an identifier. A prefix may be used to match a sha256 identifier
|
||||||
|
// within a list of trusted identifiers.
|
||||||
|
const ShortIdentifierRegexp = match(/([a-f0-9]{6,64})/);
|
||||||
|
// anchoredIdentifierRegexp is used to check or match an
|
||||||
|
// identifier value, anchored at start and end of string.
|
||||||
|
const anchoredIdentifierRegexp = anchored(IdentifierRegexp);
|
||||||
|
// anchoredShortIdentifierRegexp is used to check if a value
|
||||||
|
// is a possible identifier prefix, anchored at start and end
|
||||||
|
// of string.
|
||||||
|
const anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp);
|
||||||
|
class ImageReference {
|
||||||
|
constructor(domain, path, version) {
|
||||||
|
this.domain = domain === null || domain === void 0 ? void 0 : domain.toLowerCase();
|
||||||
|
this.path = path === null || path === void 0 ? void 0 : path.toLowerCase();
|
||||||
|
if (version) {
|
||||||
|
this.tag = version.tag;
|
||||||
|
this.digest = version.digest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get image() {
|
||||||
|
const components = this.path.split('/');
|
||||||
|
return components[components.length - 1];
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
let s = '';
|
||||||
|
if (this.domain) {
|
||||||
|
s += this.domain + '/';
|
||||||
|
}
|
||||||
|
s += this.path;
|
||||||
|
if (this.tag) {
|
||||||
|
s += ':' + this.tag;
|
||||||
|
}
|
||||||
|
if (this.digest) {
|
||||||
|
s += '@' + this.digest;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
static fromString(s) {
|
||||||
|
const matches = s.match(ReferenceRegexp);
|
||||||
|
if (matches == null) {
|
||||||
|
throw new Error(`invalid image reference`);
|
||||||
|
}
|
||||||
|
const name = matches[1], tag = matches[2], digest = matches[3];
|
||||||
|
if (name.length > nameMaxLength) {
|
||||||
|
throw new Error(`repository name must not be more than ${nameMaxLength} characters`);
|
||||||
|
}
|
||||||
|
const nameMatches = name.match(anchoredNameRegexp);
|
||||||
|
if (nameMatches == null) {
|
||||||
|
throw new Error(`invalid image reference`);
|
||||||
|
}
|
||||||
|
const domain = nameMatches[1], path = nameMatches[2];
|
||||||
|
return new ImageReference(domain, path, { tag, digest });
|
||||||
|
}
|
||||||
|
static sanitize(s) {
|
||||||
|
return this.fromString(s).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.ImageReference = ImageReference;
|
||||||
|
//# sourceMappingURL=image-reference.js.map
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 185:
|
/***/ 185:
|
||||||
|
@ -13010,6 +13198,7 @@ const tmp = __importStar(__webpack_require__(517));
|
||||||
const core = __importStar(__webpack_require__(186));
|
const core = __importStar(__webpack_require__(186));
|
||||||
const github = __importStar(__webpack_require__(438));
|
const github = __importStar(__webpack_require__(438));
|
||||||
const buildx = __importStar(__webpack_require__(295));
|
const buildx = __importStar(__webpack_require__(295));
|
||||||
|
const image_reference_1 = __webpack_require__(184);
|
||||||
let _defaultContext, _tmpDir;
|
let _defaultContext, _tmpDir;
|
||||||
function defaultContext() {
|
function defaultContext() {
|
||||||
var _a, _b;
|
var _a, _b;
|
||||||
|
@ -13077,7 +13266,7 @@ function getBuildArgs(inputs, defaultContext, buildxVersion) {
|
||||||
args.push('--label', label);
|
args.push('--label', label);
|
||||||
}));
|
}));
|
||||||
yield exports.asyncForEach(inputs.tags, (tag) => __awaiter(this, void 0, void 0, function* () {
|
yield exports.asyncForEach(inputs.tags, (tag) => __awaiter(this, void 0, void 0, function* () {
|
||||||
args.push('--tag', tag);
|
args.push('--tag', image_reference_1.ImageReference.sanitize(tag));
|
||||||
}));
|
}));
|
||||||
if (inputs.target) {
|
if (inputs.target) {
|
||||||
args.push('--target', inputs.target);
|
args.push('--target', inputs.target);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as core from '@actions/core';
|
||||||
import * as github from '@actions/github';
|
import * as github from '@actions/github';
|
||||||
|
|
||||||
import * as buildx from './buildx';
|
import * as buildx from './buildx';
|
||||||
|
import {ImageReference} from './image-reference';
|
||||||
|
|
||||||
let _defaultContext, _tmpDir: string;
|
let _defaultContext, _tmpDir: string;
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ async function getBuildArgs(inputs: Inputs, defaultContext: string, buildxVersio
|
||||||
args.push('--label', label);
|
args.push('--label', label);
|
||||||
});
|
});
|
||||||
await asyncForEach(inputs.tags, async tag => {
|
await asyncForEach(inputs.tags, async tag => {
|
||||||
args.push('--tag', tag);
|
args.push('--tag', ImageReference.sanitize(tag));
|
||||||
});
|
});
|
||||||
if (inputs.target) {
|
if (inputs.target) {
|
||||||
args.push('--target', inputs.target);
|
args.push('--target', inputs.target);
|
||||||
|
|
238
src/image-reference.ts
Normal file
238
src/image-reference.ts
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
// Grammar
|
||||||
|
//
|
||||||
|
// reference := name [ ":" tag ] [ "@" digest ]
|
||||||
|
// name := [domain '/'] path-component ['/' path-component]*
|
||||||
|
// domain := domain-component ['.' domain-component]* [':' port-number]
|
||||||
|
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
|
||||||
|
// port-number := /[0-9]+/
|
||||||
|
// path-component := alpha-numeric [separator alpha-numeric]*
|
||||||
|
// alpha-numeric := /[a-z0-9]+/
|
||||||
|
// separator := /[_.]|__|[-]*/
|
||||||
|
//
|
||||||
|
// tag := /[\w][\w.-]{0,127}/
|
||||||
|
//
|
||||||
|
// digest := digest-algorithm ":" digest-hex
|
||||||
|
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
|
||||||
|
// digest-algorithm-separator := /[+.-_]/
|
||||||
|
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
|
||||||
|
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
|
||||||
|
//
|
||||||
|
// identifier := /[a-f0-9]{64}/
|
||||||
|
// short-identifier := /[a-f0-9]{6,64}/
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/distribution/distribution/blob/master/reference/reference.go
|
||||||
|
// Ref: https://github.com/distribution/distribution/blob/master/reference/regexp.go
|
||||||
|
// Ref: https://github.com/moby/moby/blob/master/image/spec/v1.2.md
|
||||||
|
// Ref: https://github.com/jkcfg/kubernetes/blob/master/src/image-reference.ts
|
||||||
|
|
||||||
|
// nameMaxLength is the maximum total number of characters in a repository name.
|
||||||
|
const nameMaxLength = 255;
|
||||||
|
|
||||||
|
function match(s: string | RegExp): RegExp {
|
||||||
|
if (s instanceof RegExp) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return new RegExp(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteMeta(s: string): string {
|
||||||
|
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
// literal compiles s into a literal regular expression, escaping any regexp
|
||||||
|
// reserved characters.
|
||||||
|
function literal(s: string): RegExp {
|
||||||
|
return match(quoteMeta(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
// expression defines a full expression, where each regular expression must
|
||||||
|
// follow the previous.
|
||||||
|
function expression(...res: RegExp[]): RegExp {
|
||||||
|
let s = '';
|
||||||
|
for (const re of res) {
|
||||||
|
s += re.source;
|
||||||
|
}
|
||||||
|
return match(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional wraps the expression in a non-capturing group and makes the
|
||||||
|
// production optional.
|
||||||
|
function optional(...res: RegExp[]): RegExp {
|
||||||
|
return match(group(expression(...res)).source + '?');
|
||||||
|
}
|
||||||
|
|
||||||
|
// repeated wraps the regexp in a non-capturing group to get one or more
|
||||||
|
// matches.
|
||||||
|
function repeated(...res: RegExp[]): RegExp {
|
||||||
|
return match(group(expression(...res)).source + '+');
|
||||||
|
}
|
||||||
|
|
||||||
|
// capture wraps the expression in a capturing group.
|
||||||
|
function capture(...res: RegExp[]): RegExp {
|
||||||
|
return match(`(` + expression(...res).source + `)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// anchored anchors the regular expression by adding start and end delimiters.
|
||||||
|
function anchored(...res: RegExp[]): RegExp {
|
||||||
|
return match(`^` + expression(...res).source + `$`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// group wraps the regexp in a non-capturing group.
|
||||||
|
function group(...res: RegExp[]): RegExp {
|
||||||
|
return match(`(?:${expression(...res).source})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// alphaNumericRegexp defines the alpha numeric atom, typically a component of
|
||||||
|
// names. Can contain upper case characters compared to the default API to
|
||||||
|
// fix https://github.com/docker/build-push-action/issues/237#issue-748654527
|
||||||
|
const alphaNumericRegexp = match(/[a-zA-Z0-9]+/);
|
||||||
|
|
||||||
|
// separatorRegexp defines the separators allowed to be embedded in name components.
|
||||||
|
// This allow one period, one or two underscore and multiple dashes.
|
||||||
|
const separatorRegexp = match(/(?:[._]|__|[-]*)/);
|
||||||
|
|
||||||
|
// nameComponentRegexp restricts registry path component names to start with at
|
||||||
|
// least one letter or number, with following parts able to be separated by one
|
||||||
|
// period, one or two underscore and multiple dashes.
|
||||||
|
const nameComponentRegexp = expression(alphaNumericRegexp, optional(repeated(separatorRegexp, alphaNumericRegexp)));
|
||||||
|
|
||||||
|
// domainComponentRegexp restricts the registry domain component of a
|
||||||
|
// repository name to start with a component as defined by DomainRegexp
|
||||||
|
// and followed by an optional port.
|
||||||
|
const domainComponentRegexp = match(/(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/);
|
||||||
|
|
||||||
|
// DomainRegexp defines the structure of potential domain components
|
||||||
|
// that may be part of image names. This is purposely a subset of what is
|
||||||
|
// allowed by DNS to ensure backwards compatibility with Docker image
|
||||||
|
// names.
|
||||||
|
const DomainRegexp = expression(
|
||||||
|
domainComponentRegexp,
|
||||||
|
optional(repeated(literal(`.`), domainComponentRegexp)),
|
||||||
|
optional(literal(`:`), match(/[0-9]+/))
|
||||||
|
);
|
||||||
|
|
||||||
|
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
||||||
|
const TagRegexp = match(/[\w][\w.-]{0,127}/);
|
||||||
|
|
||||||
|
// anchoredTagRegexp matches valid tag names, anchored at the start and
|
||||||
|
// end of the matched string.
|
||||||
|
const anchoredTagRegexp = anchored(TagRegexp);
|
||||||
|
|
||||||
|
// DigestRegexp matches valid digests.
|
||||||
|
const DigestRegexp = match(/[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9a-fA-F]{32,}/);
|
||||||
|
|
||||||
|
// anchoredDigestRegexp matches valid digests, anchored at the start and
|
||||||
|
// end of the matched string.
|
||||||
|
const anchoredDigestRegexp = anchored(DigestRegexp);
|
||||||
|
|
||||||
|
// NameRegexp is the format for the name component of references. The
|
||||||
|
// regexp has capturing groups for the domain and name part omitting
|
||||||
|
// the separating forward slash from either.
|
||||||
|
const NameRegexp = expression(
|
||||||
|
optional(DomainRegexp, literal(`/`)),
|
||||||
|
nameComponentRegexp,
|
||||||
|
optional(repeated(literal(`/`), nameComponentRegexp))
|
||||||
|
);
|
||||||
|
|
||||||
|
// anchoredNameRegexp is used to parse a name value, capturing the
|
||||||
|
// domain and trailing components.
|
||||||
|
const anchoredNameRegexp = anchored(
|
||||||
|
optional(capture(DomainRegexp), literal(`/`)),
|
||||||
|
capture(nameComponentRegexp, optional(repeated(literal(`/`), nameComponentRegexp)))
|
||||||
|
);
|
||||||
|
|
||||||
|
// ReferenceRegexp is the full supported format of a reference. The regexp
|
||||||
|
// is anchored and has capturing groups for name, tag, and digest
|
||||||
|
// components.
|
||||||
|
const ReferenceRegexp = anchored(
|
||||||
|
capture(NameRegexp),
|
||||||
|
optional(literal(':'), capture(TagRegexp)),
|
||||||
|
optional(literal('@'), capture(DigestRegexp))
|
||||||
|
);
|
||||||
|
|
||||||
|
// IdentifierRegexp is the format for string identifier used as a
|
||||||
|
// content addressable identifier using sha256. These identifiers
|
||||||
|
// are like digests without the algorithm, since sha256 is used.
|
||||||
|
const IdentifierRegexp = match(/([a-f0-9]{64})/);
|
||||||
|
|
||||||
|
// ShortIdentifierRegexp is the format used to represent a prefix
|
||||||
|
// of an identifier. A prefix may be used to match a sha256 identifier
|
||||||
|
// within a list of trusted identifiers.
|
||||||
|
const ShortIdentifierRegexp = match(/([a-f0-9]{6,64})/);
|
||||||
|
|
||||||
|
// anchoredIdentifierRegexp is used to check or match an
|
||||||
|
// identifier value, anchored at start and end of string.
|
||||||
|
const anchoredIdentifierRegexp = anchored(IdentifierRegexp);
|
||||||
|
|
||||||
|
// anchoredShortIdentifierRegexp is used to check if a value
|
||||||
|
// is a possible identifier prefix, anchored at start and end
|
||||||
|
// of string.
|
||||||
|
const anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp);
|
||||||
|
|
||||||
|
interface VersionInfo {
|
||||||
|
tag?: string;
|
||||||
|
digest?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageReference {
|
||||||
|
domain?: string;
|
||||||
|
path: string;
|
||||||
|
tag?: string;
|
||||||
|
digest?: string;
|
||||||
|
|
||||||
|
constructor(domain: string | undefined, path: string, version?: VersionInfo) {
|
||||||
|
this.domain = domain?.toLowerCase();
|
||||||
|
this.path = path?.toLowerCase();
|
||||||
|
if (version) {
|
||||||
|
this.tag = version.tag;
|
||||||
|
this.digest = version.digest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get image(): string {
|
||||||
|
const components = this.path.split('/');
|
||||||
|
return components[components.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
let s = '';
|
||||||
|
if (this.domain) {
|
||||||
|
s += this.domain + '/';
|
||||||
|
}
|
||||||
|
s += this.path;
|
||||||
|
if (this.tag) {
|
||||||
|
s += ':' + this.tag;
|
||||||
|
}
|
||||||
|
if (this.digest) {
|
||||||
|
s += '@' + this.digest;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromString(s: string): ImageReference {
|
||||||
|
const matches = s.match(ReferenceRegexp);
|
||||||
|
if (matches == null) {
|
||||||
|
throw new Error(`invalid image reference`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = matches[1],
|
||||||
|
tag = matches[2],
|
||||||
|
digest = matches[3];
|
||||||
|
if (name.length > nameMaxLength) {
|
||||||
|
throw new Error(`repository name must not be more than ${nameMaxLength} characters`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameMatches = name.match(anchoredNameRegexp);
|
||||||
|
if (nameMatches == null) {
|
||||||
|
throw new Error(`invalid image reference`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = nameMatches[1],
|
||||||
|
path = nameMatches[2];
|
||||||
|
return new ImageReference(domain, path, {tag, digest});
|
||||||
|
}
|
||||||
|
|
||||||
|
static sanitize(s: string): string {
|
||||||
|
return this.fromString(s).toString();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue