1
0
Fork 0
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:
CrazyMax 2021-04-08 16:07:52 +02:00
parent 8891861577
commit 92991ec63d
No known key found for this signature in database
GPG key ID: 3248E46B6BB8C7F7
4 changed files with 616 additions and 2 deletions

View 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
View file

@ -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);

View file

@ -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
View 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();
}
}