@softvisio/core
Version:
Softisio core
177 lines (125 loc) • 5.13 kB
JavaScript
import fetch from "#lib/fetch";
// NOTE https://docs.docker.com/registry/spec/api/
const DEFAULT_REGISTRY = "index.docker.io";
const API_VERSION = "v2";
export default class DockerRegistry {
#username;
#password;
#registry;
#id;
#basicAuth;
#bearerAuth = {};
#urlAuthCache = {};
constructor ( username, password, registry ) {
this.#username = username;
this.#password = password;
this.#registry = registry || DEFAULT_REGISTRY;
}
// properties
get id () {
this.#id ??= this.#registry + ":" + this.#username;
return this.#id;
}
// public
// NOTE not works for docker hub
// NOTE for harbor works only for administrators
async getCatalog () {
const url = "https://" + this.#registry + "/" + API_VERSION + "/_catalog";
const res = await this.#request( url );
if ( !res.ok ) return res;
const data = await res.json();
return result( 200, data );
}
async getTags ( repo ) {
const url = "https://" + this.#registry + "/" + API_VERSION + "/" + repo + "/tags/list";
const res = await this.#request( url );
if ( !res.ok ) return res;
const data = await res.json();
return result( 200, data );
}
async getManifest ( repo, tag ) {
const url = "https://" + this.#registry + "/" + API_VERSION + "/" + repo + "/manifests/" + ( tag || "latest" );
const res = await this.#request( url, null, { "accept": "application/vnd.docker.distribution.manifest.v2+json" } );
if ( !res.ok ) return res;
const data = await res.json();
data.digest = res.headers.get( "docker-content-digest" );
return result( 200, data );
}
async deleteImage ( repo, tag ) {
const manifest = await this.getManifest( repo, tag );
if ( !manifest.ok ) return manifest;
const url = "https://" + this.#registry + "/" + API_VERSION + "/" + repo + "/manifests/" + manifest.data.digest;
return this.#request( url, "delete" );
}
// private
async #request ( url, method, headers = {} ) {
var authorization = this.#urlAuthCache[ url ];
if ( authorization ) {
if ( authorization.basic ) authorization = authorization.basic;
else if ( authorization.bearer ) authorization = await this.#getBearerAuth( authorization.bearer );
}
while ( true ) {
const res = await fetch( url, {
method,
"headers": {
...headers,
authorization,
},
} );
if ( !res.ok ) {
// authorization error
if ( res.status === 401 ) {
// invalid auth
if ( authorization ) return result( res );
const wwwAuthenticate = res.headers.wwwAuthenticate;
if ( !wwwAuthenticate ) return result( res );
if ( wwwAuthenticate.scheme === "basic" ) {
authorization = this.#getBasicAuth();
this.#urlAuthCache[ url ] ||= {};
this.#urlAuthCache[ url ].basic = this.#getBasicAuth();
}
else if ( wwwAuthenticate.scheme === "bearer" ) {
const authUrl = wwwAuthenticate.realm + "?service=" + wwwAuthenticate.service + "&scope=" + wwwAuthenticate.scope;
authorization = await this.#getBearerAuth( authUrl );
// unable to get bearer token
if ( !authorization ) return result( res );
this.#urlAuthCache[ url ] ||= {};
this.#urlAuthCache[ url ].bearer = authUrl;
}
else {
return result( [ 500, "Invalid authentication scheme" ] );
}
// repeat request with authorization
continue;
}
// other error
else {
return result( res );
}
}
// ok
else {
return res;
}
}
}
#getBasicAuth () {
this.#basicAuth ??= "Basic " + Buffer.from( this.#username + ":" + this.#password ).toString( "base64" );
return this.#basicAuth;
}
async #getBearerAuth ( url ) {
if ( this.#bearerAuth[ url ] && this.#bearerAuth[ url ].expires > Date.now() ) return this.#bearerAuth[ url ].token;
const res = await fetch( url, {
"headers": {
"authorization": this.#getBasicAuth(),
},
} );
if ( !res.ok ) return;
const json = await res.json();
this.#bearerAuth[ url ] = {
"expires": Date.parse( json.issued_at ) + json.expires_in * 1000 - 3000,
"token": "Bearer " + json.token,
};
return this.#bearerAuth[ url ].token;
}
}