@kustodian/registry 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/auth.ts +51 -0
- package/src/client.ts +95 -0
- package/src/dockerhub.ts +130 -0
- package/src/ghcr.ts +131 -0
- package/src/index.ts +32 -0
- package/src/types.ts +60 -0
- package/src/version.ts +87 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kustodian/registry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Container registry client for fetching image tags from Docker Hub and GHCR",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"test:watch": "bun test --watch",
|
|
20
|
+
"typecheck": "bun run tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"kustodian",
|
|
24
|
+
"registry",
|
|
25
|
+
"docker",
|
|
26
|
+
"ghcr",
|
|
27
|
+
"container"
|
|
28
|
+
],
|
|
29
|
+
"author": "Luca Silverentand <luca@onezero.company>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/lucasilverentand/kustodian.git",
|
|
34
|
+
"directory": "packages/registry"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://npm.pkg.github.com"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@kustodian/core": "workspace:*",
|
|
41
|
+
"semver": "^7.6.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/semver": "^7.5.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RegistryAuthType } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets authentication for Docker Hub from environment variables.
|
|
5
|
+
*/
|
|
6
|
+
export function get_dockerhub_auth(): RegistryAuthType | undefined {
|
|
7
|
+
const username = process.env['DOCKER_USERNAME'];
|
|
8
|
+
const password = process.env['DOCKER_PASSWORD'];
|
|
9
|
+
|
|
10
|
+
if (username && password) {
|
|
11
|
+
return { username, password };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gets authentication for GHCR from environment variables.
|
|
19
|
+
*/
|
|
20
|
+
export function get_ghcr_auth(): RegistryAuthType | undefined {
|
|
21
|
+
const token = process.env['GITHUB_TOKEN'] || process.env['GH_TOKEN'];
|
|
22
|
+
|
|
23
|
+
if (token) {
|
|
24
|
+
return { token };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets authentication for a registry based on hostname.
|
|
32
|
+
*/
|
|
33
|
+
export function get_auth_for_registry(registry: string): RegistryAuthType | undefined {
|
|
34
|
+
if (registry === 'docker.io' || registry === 'registry.hub.docker.com') {
|
|
35
|
+
return get_dockerhub_auth();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (registry === 'ghcr.io') {
|
|
39
|
+
return get_ghcr_auth();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Generic fallback
|
|
43
|
+
const username = process.env['REGISTRY_USERNAME'];
|
|
44
|
+
const password = process.env['REGISTRY_PASSWORD'];
|
|
45
|
+
|
|
46
|
+
if (username && password) {
|
|
47
|
+
return { username, password };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { get_auth_for_registry } from './auth.js';
|
|
2
|
+
import { create_dockerhub_client } from './dockerhub.js';
|
|
3
|
+
import { create_ghcr_client } from './ghcr.js';
|
|
4
|
+
import type { ImageReferenceType, RegistryClientConfigType, RegistryClientType } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses an image string into its components.
|
|
8
|
+
*
|
|
9
|
+
* Supports various formats:
|
|
10
|
+
* - nginx -> docker.io/library/nginx
|
|
11
|
+
* - prom/prometheus -> docker.io/prom/prometheus
|
|
12
|
+
* - ghcr.io/org/image -> ghcr.io/org/image
|
|
13
|
+
* - ghcr.io/org/image:tag -> ghcr.io/org/image:tag
|
|
14
|
+
*/
|
|
15
|
+
export function parse_image_reference(image: string): ImageReferenceType {
|
|
16
|
+
let registry = 'docker.io';
|
|
17
|
+
let namespace = 'library';
|
|
18
|
+
let repository: string;
|
|
19
|
+
let tag: string | undefined;
|
|
20
|
+
|
|
21
|
+
// Split tag if present
|
|
22
|
+
const colonIndex = image.lastIndexOf(':');
|
|
23
|
+
let imagePart = image;
|
|
24
|
+
|
|
25
|
+
// Only treat as tag if there's no slash after the colon (to handle ports)
|
|
26
|
+
if (colonIndex > 0 && !image.slice(colonIndex).includes('/')) {
|
|
27
|
+
imagePart = image.slice(0, colonIndex);
|
|
28
|
+
tag = image.slice(colonIndex + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parts = imagePart.split('/');
|
|
32
|
+
|
|
33
|
+
if (parts.length === 1 && parts[0]) {
|
|
34
|
+
// Simple image name: nginx -> docker.io/library/nginx
|
|
35
|
+
repository = parts[0];
|
|
36
|
+
} else if (parts.length === 2 && parts[0] && parts[1]) {
|
|
37
|
+
if (parts[0].includes('.') || parts[0].includes(':')) {
|
|
38
|
+
// Custom registry without namespace: registry.io/image
|
|
39
|
+
registry = parts[0];
|
|
40
|
+
namespace = 'library';
|
|
41
|
+
repository = parts[1];
|
|
42
|
+
} else {
|
|
43
|
+
// Docker Hub with namespace: prom/prometheus
|
|
44
|
+
namespace = parts[0];
|
|
45
|
+
repository = parts[1];
|
|
46
|
+
}
|
|
47
|
+
} else if (parts.length >= 3 && parts[0] && parts[1]) {
|
|
48
|
+
// Full path: ghcr.io/org/image
|
|
49
|
+
registry = parts[0];
|
|
50
|
+
namespace = parts[1];
|
|
51
|
+
repository = parts.slice(2).join('/');
|
|
52
|
+
} else {
|
|
53
|
+
// Fallback - should not happen with valid input
|
|
54
|
+
repository = imagePart;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { registry, namespace, repository, tag };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detects the registry type from an image reference.
|
|
62
|
+
*/
|
|
63
|
+
export function detect_registry_type(image: ImageReferenceType): 'dockerhub' | 'ghcr' {
|
|
64
|
+
if (image.registry === 'ghcr.io') {
|
|
65
|
+
return 'ghcr';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Default to Docker Hub
|
|
69
|
+
return 'dockerhub';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a registry client for the given registry type.
|
|
74
|
+
*/
|
|
75
|
+
export function create_registry_client(
|
|
76
|
+
registry_type: 'dockerhub' | 'ghcr',
|
|
77
|
+
config?: RegistryClientConfigType,
|
|
78
|
+
): RegistryClientType {
|
|
79
|
+
switch (registry_type) {
|
|
80
|
+
case 'ghcr':
|
|
81
|
+
return create_ghcr_client(config);
|
|
82
|
+
default:
|
|
83
|
+
return create_dockerhub_client(config);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a registry client with auto-detected type and authentication.
|
|
89
|
+
*/
|
|
90
|
+
export function create_client_for_image(image: ImageReferenceType): RegistryClientType {
|
|
91
|
+
const registry_type = detect_registry_type(image);
|
|
92
|
+
const auth = get_auth_for_registry(image.registry);
|
|
93
|
+
|
|
94
|
+
return create_registry_client(registry_type, { auth });
|
|
95
|
+
}
|
package/src/dockerhub.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { type ResultType, failure, success } from '@kustodian/core';
|
|
2
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
3
|
+
import type {
|
|
4
|
+
ImageReferenceType,
|
|
5
|
+
RegistryClientConfigType,
|
|
6
|
+
RegistryClientType,
|
|
7
|
+
TagInfoType,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const DOCKERHUB_AUTH_URL = 'https://auth.docker.io/token';
|
|
11
|
+
const DOCKERHUB_REGISTRY_URL = 'https://registry-1.docker.io/v2';
|
|
12
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
13
|
+
|
|
14
|
+
interface DockerHubTokenResponse {
|
|
15
|
+
token: string;
|
|
16
|
+
access_token?: string;
|
|
17
|
+
expires_in?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DockerHubTagsResponse {
|
|
21
|
+
name: string;
|
|
22
|
+
tags: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an abort signal with timeout.
|
|
27
|
+
*/
|
|
28
|
+
function create_abort_signal(timeout_ms: number): AbortSignal {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
setTimeout(() => controller.abort(), timeout_ms);
|
|
31
|
+
return controller.signal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gets an authentication token for Docker Hub.
|
|
36
|
+
*/
|
|
37
|
+
async function get_dockerhub_token(
|
|
38
|
+
image: ImageReferenceType,
|
|
39
|
+
config?: RegistryClientConfigType,
|
|
40
|
+
): Promise<ResultType<string, KustodianErrorType>> {
|
|
41
|
+
const scope = `repository:${image.namespace}/${image.repository}:pull`;
|
|
42
|
+
const url = `${DOCKERHUB_AUTH_URL}?service=registry.docker.io&scope=${encodeURIComponent(scope)}`;
|
|
43
|
+
|
|
44
|
+
const headers: Record<string, string> = {};
|
|
45
|
+
|
|
46
|
+
if (config?.auth?.username && config.auth.password) {
|
|
47
|
+
const credentials = Buffer.from(`${config.auth.username}:${config.auth.password}`).toString(
|
|
48
|
+
'base64',
|
|
49
|
+
);
|
|
50
|
+
headers['Authorization'] = `Basic ${credentials}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(url, {
|
|
55
|
+
headers,
|
|
56
|
+
signal: create_abort_signal(config?.timeout ?? DEFAULT_TIMEOUT),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
return failure({
|
|
61
|
+
code: 'REGISTRY_AUTH_ERROR',
|
|
62
|
+
message: `Docker Hub auth failed: ${response.status} ${response.statusText}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = (await response.json()) as DockerHubTokenResponse;
|
|
67
|
+
const token = data.token || data.access_token;
|
|
68
|
+
|
|
69
|
+
if (!token) {
|
|
70
|
+
return failure({
|
|
71
|
+
code: 'REGISTRY_AUTH_ERROR',
|
|
72
|
+
message: 'No token returned from Docker Hub auth',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return success(token);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return failure({
|
|
79
|
+
code: 'REGISTRY_ERROR',
|
|
80
|
+
message: `Docker Hub auth request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates a Docker Hub registry client.
|
|
87
|
+
*/
|
|
88
|
+
export function create_dockerhub_client(config?: RegistryClientConfigType): RegistryClientType {
|
|
89
|
+
return {
|
|
90
|
+
async list_tags(
|
|
91
|
+
image: ImageReferenceType,
|
|
92
|
+
): Promise<ResultType<TagInfoType[], KustodianErrorType>> {
|
|
93
|
+
// Get auth token
|
|
94
|
+
const token_result = await get_dockerhub_token(image, config);
|
|
95
|
+
if (!token_result.success) {
|
|
96
|
+
return token_result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const url = `${DOCKERHUB_REGISTRY_URL}/${image.namespace}/${image.repository}/tags/list`;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${token_result.value}`,
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
},
|
|
107
|
+
signal: create_abort_signal(config?.timeout ?? DEFAULT_TIMEOUT),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
return failure({
|
|
112
|
+
code: 'REGISTRY_ERROR',
|
|
113
|
+
message: `Failed to fetch tags: ${response.status} ${response.statusText}`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = (await response.json()) as DockerHubTagsResponse;
|
|
118
|
+
|
|
119
|
+
const tags: TagInfoType[] = (data.tags || []).map((name) => ({ name }));
|
|
120
|
+
|
|
121
|
+
return success(tags);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return failure({
|
|
124
|
+
code: 'REGISTRY_ERROR',
|
|
125
|
+
message: `Failed to fetch tags: ${error instanceof Error ? error.message : String(error)}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
package/src/ghcr.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { type ResultType, failure, success } from '@kustodian/core';
|
|
2
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
3
|
+
import type {
|
|
4
|
+
ImageReferenceType,
|
|
5
|
+
RegistryClientConfigType,
|
|
6
|
+
RegistryClientType,
|
|
7
|
+
TagInfoType,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const GHCR_URL = 'https://ghcr.io/v2';
|
|
11
|
+
const GHCR_AUTH_URL = 'https://ghcr.io/token';
|
|
12
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
13
|
+
|
|
14
|
+
interface GhcrTokenResponse {
|
|
15
|
+
token: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface GhcrTagsResponse {
|
|
19
|
+
name: string;
|
|
20
|
+
tags: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an abort signal with timeout.
|
|
25
|
+
*/
|
|
26
|
+
function create_abort_signal(timeout_ms: number): AbortSignal {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
setTimeout(() => controller.abort(), timeout_ms);
|
|
29
|
+
return controller.signal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets an authentication token for GHCR.
|
|
34
|
+
*/
|
|
35
|
+
async function get_ghcr_token(
|
|
36
|
+
image: ImageReferenceType,
|
|
37
|
+
config?: RegistryClientConfigType,
|
|
38
|
+
): Promise<ResultType<string, KustodianErrorType>> {
|
|
39
|
+
const scope = `repository:${image.namespace}/${image.repository}:pull`;
|
|
40
|
+
const url = `${GHCR_AUTH_URL}?service=ghcr.io&scope=${encodeURIComponent(scope)}`;
|
|
41
|
+
|
|
42
|
+
const headers: Record<string, string> = {};
|
|
43
|
+
|
|
44
|
+
// GHCR supports token-based auth or username/password
|
|
45
|
+
if (config?.auth?.token) {
|
|
46
|
+
// Use bearer token (GitHub PAT)
|
|
47
|
+
headers['Authorization'] = `Bearer ${config.auth.token}`;
|
|
48
|
+
} else if (config?.auth?.username && config.auth.password) {
|
|
49
|
+
const credentials = Buffer.from(`${config.auth.username}:${config.auth.password}`).toString(
|
|
50
|
+
'base64',
|
|
51
|
+
);
|
|
52
|
+
headers['Authorization'] = `Basic ${credentials}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
headers,
|
|
58
|
+
signal: create_abort_signal(config?.timeout ?? DEFAULT_TIMEOUT),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
return failure({
|
|
63
|
+
code: 'REGISTRY_AUTH_ERROR',
|
|
64
|
+
message: `GHCR auth failed: ${response.status} ${response.statusText}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const data = (await response.json()) as GhcrTokenResponse;
|
|
69
|
+
|
|
70
|
+
if (!data.token) {
|
|
71
|
+
return failure({
|
|
72
|
+
code: 'REGISTRY_AUTH_ERROR',
|
|
73
|
+
message: 'No token returned from GHCR auth',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return success(data.token);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return failure({
|
|
80
|
+
code: 'REGISTRY_ERROR',
|
|
81
|
+
message: `GHCR auth request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a GitHub Container Registry client.
|
|
88
|
+
*/
|
|
89
|
+
export function create_ghcr_client(config?: RegistryClientConfigType): RegistryClientType {
|
|
90
|
+
return {
|
|
91
|
+
async list_tags(
|
|
92
|
+
image: ImageReferenceType,
|
|
93
|
+
): Promise<ResultType<TagInfoType[], KustodianErrorType>> {
|
|
94
|
+
// Get auth token
|
|
95
|
+
const token_result = await get_ghcr_token(image, config);
|
|
96
|
+
if (!token_result.success) {
|
|
97
|
+
return token_result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const url = `${GHCR_URL}/${image.namespace}/${image.repository}/tags/list`;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(url, {
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${token_result.value}`,
|
|
106
|
+
Accept: 'application/json',
|
|
107
|
+
},
|
|
108
|
+
signal: create_abort_signal(config?.timeout ?? DEFAULT_TIMEOUT),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
return failure({
|
|
113
|
+
code: 'REGISTRY_ERROR',
|
|
114
|
+
message: `Failed to fetch tags: ${response.status} ${response.statusText}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const data = (await response.json()) as GhcrTagsResponse;
|
|
119
|
+
|
|
120
|
+
const tags: TagInfoType[] = (data.tags || []).map((name) => ({ name }));
|
|
121
|
+
|
|
122
|
+
return success(tags);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return failure({
|
|
125
|
+
code: 'REGISTRY_ERROR',
|
|
126
|
+
message: `Failed to fetch tags: ${error instanceof Error ? error.message : String(error)}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
ImageReferenceType,
|
|
4
|
+
RegistryAuthType,
|
|
5
|
+
RegistryClientConfigType,
|
|
6
|
+
RegistryClientType,
|
|
7
|
+
TagInfoType,
|
|
8
|
+
VersionCheckResultType,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
// Client utilities
|
|
12
|
+
export {
|
|
13
|
+
parse_image_reference,
|
|
14
|
+
detect_registry_type,
|
|
15
|
+
create_registry_client,
|
|
16
|
+
create_client_for_image,
|
|
17
|
+
} from './client.js';
|
|
18
|
+
|
|
19
|
+
// Registry implementations
|
|
20
|
+
export { create_dockerhub_client } from './dockerhub.js';
|
|
21
|
+
export { create_ghcr_client } from './ghcr.js';
|
|
22
|
+
|
|
23
|
+
// Auth utilities
|
|
24
|
+
export { get_dockerhub_auth, get_ghcr_auth, get_auth_for_registry } from './auth.js';
|
|
25
|
+
|
|
26
|
+
// Version utilities
|
|
27
|
+
export {
|
|
28
|
+
DEFAULT_SEMVER_PATTERN,
|
|
29
|
+
filter_semver_tags,
|
|
30
|
+
find_latest_matching,
|
|
31
|
+
check_version_update,
|
|
32
|
+
} from './version.js';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { KustodianErrorType, ResultType } from '@kustodian/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authentication configuration for registries.
|
|
5
|
+
*/
|
|
6
|
+
export interface RegistryAuthType {
|
|
7
|
+
username?: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
token?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Registry client configuration.
|
|
14
|
+
*/
|
|
15
|
+
export interface RegistryClientConfigType {
|
|
16
|
+
auth?: RegistryAuthType | undefined;
|
|
17
|
+
timeout?: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parsed image reference components.
|
|
22
|
+
*/
|
|
23
|
+
export interface ImageReferenceType {
|
|
24
|
+
/** Registry hostname (e.g., docker.io, ghcr.io) */
|
|
25
|
+
registry: string;
|
|
26
|
+
/** Namespace/organization (e.g., library, prom) */
|
|
27
|
+
namespace: string;
|
|
28
|
+
/** Repository name (e.g., nginx, prometheus) */
|
|
29
|
+
repository: string;
|
|
30
|
+
/** Optional tag */
|
|
31
|
+
tag?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tag information from registry.
|
|
36
|
+
*/
|
|
37
|
+
export interface TagInfoType {
|
|
38
|
+
name: string;
|
|
39
|
+
digest?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Version check result.
|
|
44
|
+
*/
|
|
45
|
+
export interface VersionCheckResultType {
|
|
46
|
+
current_version: string;
|
|
47
|
+
latest_version: string;
|
|
48
|
+
available_versions: string[];
|
|
49
|
+
has_update: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Registry client interface.
|
|
54
|
+
*/
|
|
55
|
+
export interface RegistryClientType {
|
|
56
|
+
/**
|
|
57
|
+
* Lists all tags for an image.
|
|
58
|
+
*/
|
|
59
|
+
list_tags(image: ImageReferenceType): Promise<ResultType<TagInfoType[], KustodianErrorType>>;
|
|
60
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as semver from 'semver';
|
|
2
|
+
import type { TagInfoType, VersionCheckResultType } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default pattern for semver-like tags.
|
|
6
|
+
* Matches: 1.0.0, v1.0.0, 1.0.0-alpha, v1.0.0-rc.1
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_SEMVER_PATTERN = /^v?(\d+\.\d+\.\d+)(-[\w.]+)?$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Filters tags to only semver-valid versions.
|
|
12
|
+
*/
|
|
13
|
+
export function filter_semver_tags(
|
|
14
|
+
tags: TagInfoType[],
|
|
15
|
+
options: {
|
|
16
|
+
pattern?: RegExp;
|
|
17
|
+
exclude_prerelease?: boolean;
|
|
18
|
+
} = {},
|
|
19
|
+
): string[] {
|
|
20
|
+
const pattern = options.pattern ?? DEFAULT_SEMVER_PATTERN;
|
|
21
|
+
const exclude_prerelease = options.exclude_prerelease ?? true;
|
|
22
|
+
|
|
23
|
+
const versions: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (const tag of tags) {
|
|
26
|
+
if (!pattern.test(tag.name)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cleaned = semver.clean(tag.name);
|
|
31
|
+
if (!cleaned) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parsed = semver.parse(cleaned);
|
|
36
|
+
if (!parsed) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (exclude_prerelease && parsed.prerelease.length > 0) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
versions.push(cleaned);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Sort descending (newest first)
|
|
48
|
+
return versions.sort(semver.rcompare);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Finds the latest version satisfying a constraint.
|
|
53
|
+
*/
|
|
54
|
+
export function find_latest_matching(versions: string[], constraint?: string): string | undefined {
|
|
55
|
+
if (!constraint) {
|
|
56
|
+
// Return the latest (first in sorted array)
|
|
57
|
+
return versions[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = semver.maxSatisfying(versions, constraint);
|
|
61
|
+
return result ?? undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if a newer version is available.
|
|
66
|
+
*/
|
|
67
|
+
export function check_version_update(
|
|
68
|
+
current: string,
|
|
69
|
+
available: string[],
|
|
70
|
+
constraint?: string,
|
|
71
|
+
): VersionCheckResultType {
|
|
72
|
+
const cleaned_current = semver.clean(current) ?? current;
|
|
73
|
+
const latest = find_latest_matching(available, constraint);
|
|
74
|
+
|
|
75
|
+
const has_update =
|
|
76
|
+
latest !== undefined &&
|
|
77
|
+
semver.valid(latest) !== null &&
|
|
78
|
+
semver.valid(cleaned_current) !== null &&
|
|
79
|
+
semver.gt(latest, cleaned_current);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
current_version: cleaned_current,
|
|
83
|
+
latest_version: latest ?? cleaned_current,
|
|
84
|
+
available_versions: available,
|
|
85
|
+
has_update,
|
|
86
|
+
};
|
|
87
|
+
}
|