mirror of
https://github.com/docker/build-push-action.git
synced 2025-03-28 01:30:09 +01: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
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 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:
|
||||
|
@ -13010,6 +13198,7 @@ const tmp = __importStar(__webpack_require__(517));
|
|||
const core = __importStar(__webpack_require__(186));
|
||||
const github = __importStar(__webpack_require__(438));
|
||||
const buildx = __importStar(__webpack_require__(295));
|
||||
const image_reference_1 = __webpack_require__(184);
|
||||
let _defaultContext, _tmpDir;
|
||||
function defaultContext() {
|
||||
var _a, _b;
|
||||
|
@ -13077,7 +13266,7 @@ function getBuildArgs(inputs, defaultContext, buildxVersion) {
|
|||
args.push('--label', label);
|
||||
}));
|
||||
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) {
|
||||
args.push('--target', inputs.target);
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as core from '@actions/core';
|
|||
import * as github from '@actions/github';
|
||||
|
||||
import * as buildx from './buildx';
|
||||
import {ImageReference} from './image-reference';
|
||||
|
||||
let _defaultContext, _tmpDir: string;
|
||||
|
||||
|
@ -97,7 +98,7 @@ async function getBuildArgs(inputs: Inputs, defaultContext: string, buildxVersio
|
|||
args.push('--label', label);
|
||||
});
|
||||
await asyncForEach(inputs.tags, async tag => {
|
||||
args.push('--tag', tag);
|
||||
args.push('--tag', ImageReference.sanitize(tag));
|
||||
});
|
||||
if (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