Skip to content

Commit 7c70587

Browse files
committed
add mautrix registry
1 parent 46f8d3e commit 7c70587

6 files changed

Lines changed: 256 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
- **Compose-native auto-compose discovery** — Added `dd.compose.native` / `wud.compose.native` container labels to enable deriving compose file paths from native Compose labels (`com.docker.compose.project.config_files` + `com.docker.compose.project.working_dir`) when `dd.compose.file` is not set. This requires the resolved compose path to exist inside the drydock container (same path context used by `docker compose`).
1919
- **Watcher-wide compose-native default** — Added `DD_WATCHER_{name}_COMPOSE_NATIVE=true` to enable compose-native path discovery for all containers watched by a Docker watcher, with per-container `dd.compose.native` still taking precedence.
20+
- **MAU registry provider (`dock.mau.dev`)** — Added a GitLab-based `mau` registry provider with token auth support. It is auto-registered as `mau.public` by default for public access and can also be configured as private via `DD_REGISTRY_MAU_{name}_TOKEN`.
2021

2122
### Fixed
2223

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ See full configuration in [`docs/configuration/security/README.md`](docs/configu
323323
| Google Artifact Registry | `gar` | `*-docker.pkg.dev` |
324324
| Quay | `quay` | `quay.io` |
325325
| LinuxServer (LSCR) | `lscr` | `lscr.io` |
326+
| MAU | `mau` | `dock.mau.dev` |
326327
| DigitalOcean | `docr` | `registry.digitalocean.com` |
327328
| Codeberg | `codeberg` | `codeberg.org` |
328329
| DHI | `dhi` | `dhi.io` |
@@ -344,6 +345,7 @@ See full configuration in [`docs/configuration/security/README.md`](docs/configu
344345
| GitLab | `gitlab` | `DD_REGISTRY_GITLAB_{name}_TOKEN` |
345346
| GitHub (GHCR) | `ghcr` | `DD_REGISTRY_GHCR_{name}_TOKEN` |
346347
| Gitea / Forgejo | `gitea` | `DD_REGISTRY_GITEA_{name}_LOGIN`, `_PASSWORD` |
348+
| MAU Registry (GitLab-based) | `mau` | `DD_REGISTRY_MAU_{name}_TOKEN` |
347349
| Harbor | `harbor` | `DD_REGISTRY_HARBOR_{name}_URL`, `_LOGIN`, `_PASSWORD` |
348350
| JFrog Artifactory | `artifactory` | `DD_REGISTRY_ARTIFACTORY_{name}_URL`, `_LOGIN`, `_PASSWORD` |
349351
| Sonatype Nexus | `nexus` | `DD_REGISTRY_NEXUS_{name}_URL`, `_LOGIN`, `_PASSWORD` |
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// @ts-nocheck
2+
import axios from 'axios';
3+
import Mau from './Mau.js';
4+
5+
const TEST_TOKEN = 'abcdef';
6+
7+
const mau = new Mau();
8+
mau.configuration = {
9+
url: 'https://dock.mau.dev',
10+
authurl: 'https://dock.mau.dev',
11+
token: TEST_TOKEN,
12+
};
13+
14+
vi.mock('axios');
15+
16+
test('validatedConfiguration should initialize when configuration is empty string', async () => {
17+
expect(mau.validateConfiguration('')).toStrictEqual({});
18+
});
19+
20+
test('validatedConfiguration should initialize when configuration is valid', async () => {
21+
expect(
22+
mau.validateConfiguration({
23+
token: TEST_TOKEN,
24+
}),
25+
).toStrictEqual({
26+
url: 'https://dock.mau.dev',
27+
authurl: 'https://dock.mau.dev',
28+
token: TEST_TOKEN,
29+
});
30+
});
31+
32+
test('maskConfiguration should mask token', async () => {
33+
expect(mau.maskConfiguration()).toEqual({
34+
url: 'https://dock.mau.dev',
35+
authurl: 'https://dock.mau.dev',
36+
token: 'a****f',
37+
});
38+
});
39+
40+
test('match should return true for dock.mau.dev', async () => {
41+
expect(
42+
mau.match({
43+
registry: {
44+
url: 'dock.mau.dev',
45+
},
46+
}),
47+
).toBeTruthy();
48+
});
49+
50+
test('match should return true for subdomains of dock.mau.dev', async () => {
51+
expect(
52+
mau.match({
53+
registry: {
54+
url: 'registry.dock.mau.dev',
55+
},
56+
}),
57+
).toBeTruthy();
58+
});
59+
60+
test('match should return false for other registries', async () => {
61+
expect(
62+
mau.match({
63+
registry: {
64+
url: 'registry.gitlab.com',
65+
},
66+
}),
67+
).toBeFalsy();
68+
});
69+
70+
test('authenticate should perform request with token auth when token is configured', async () => {
71+
axios.mockImplementation(() => ({
72+
data: {
73+
token: 'token',
74+
},
75+
}));
76+
77+
await expect(
78+
mau.authenticate(
79+
{ name: 'team/image' },
80+
{
81+
headers: {},
82+
},
83+
),
84+
).resolves.toEqual({ headers: { Authorization: 'Bearer token' } });
85+
86+
expect(axios).toHaveBeenCalledWith(
87+
expect.objectContaining({
88+
headers: expect.objectContaining({
89+
Authorization: `Basic ${Buffer.from(`:${TEST_TOKEN}`, 'utf-8').toString('base64')}`,
90+
}),
91+
}),
92+
);
93+
});
94+
95+
test('authenticate should omit basic auth when token is not configured', async () => {
96+
const mauPublic = new Mau();
97+
mauPublic.configuration = {
98+
url: 'https://dock.mau.dev',
99+
authurl: 'https://dock.mau.dev',
100+
};
101+
102+
axios.mockImplementation(() => ({
103+
data: {
104+
token: 'public-token',
105+
},
106+
}));
107+
108+
await expect(
109+
mauPublic.authenticate(
110+
{ name: 'team/image' },
111+
{
112+
headers: {},
113+
},
114+
),
115+
).resolves.toEqual({ headers: { Authorization: 'Bearer public-token' } });
116+
117+
expect(axios).toHaveBeenCalledWith(
118+
expect.objectContaining({
119+
headers: {
120+
Accept: 'application/json',
121+
},
122+
}),
123+
);
124+
});
125+
126+
test('normalizeImage should return the proper registry v2 endpoint', async () => {
127+
expect(
128+
mau.normalizeImage({
129+
name: 'test/image',
130+
registry: {
131+
url: 'dock.mau.dev',
132+
},
133+
}),
134+
).toStrictEqual({
135+
name: 'test/image',
136+
registry: {
137+
url: 'https://dock.mau.dev/v2',
138+
},
139+
});
140+
});
141+
142+
test('getAuthPull should return undefined when no token is configured', async () => {
143+
const mauPublic = new Mau();
144+
mauPublic.configuration = {
145+
url: 'https://dock.mau.dev',
146+
authurl: 'https://dock.mau.dev',
147+
};
148+
await expect(mauPublic.getAuthPull()).resolves.toBeUndefined();
149+
});
150+
151+
test('getAuthPull should return username/password when token is configured', async () => {
152+
await expect(mau.getAuthPull()).resolves.toEqual({
153+
username: '',
154+
password: TEST_TOKEN,
155+
});
156+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// @ts-nocheck
2+
import axios from 'axios';
3+
import Gitlab from '../gitlab/Gitlab.js';
4+
5+
/**
6+
* dock.mau.dev (GitLab-based) Container Registry integration.
7+
*/
8+
class Mau extends Gitlab {
9+
/**
10+
* Get the mau configuration schema.
11+
* @returns {*}
12+
*/
13+
getConfigurationSchema() {
14+
return this.joi.alternatives([
15+
this.joi.string().allow(''),
16+
this.joi.object().keys({
17+
url: this.joi.string().uri().default('https://dock.mau.dev'),
18+
authurl: this.joi.string().uri().default('https://dock.mau.dev'),
19+
token: this.joi.string(),
20+
}),
21+
]);
22+
}
23+
24+
/**
25+
* Custom init behavior.
26+
*/
27+
init() {
28+
this.configuration = this.configuration || {};
29+
if (typeof this.configuration === 'string') {
30+
this.configuration = {};
31+
}
32+
this.configuration.url = this.configuration.url || 'https://dock.mau.dev';
33+
this.configuration.authurl = this.configuration.authurl || 'https://dock.mau.dev';
34+
}
35+
36+
/**
37+
* Sanitize sensitive data.
38+
* @returns {*}
39+
*/
40+
maskConfiguration() {
41+
return this.maskSensitiveFields(['token']);
42+
}
43+
44+
/**
45+
* Return true if image registry matches dock.mau.dev.
46+
* @param image the image
47+
* @returns {boolean}
48+
*/
49+
match(image) {
50+
return /^.*\.?dock\.mau\.dev$/i.test(image.registry.url);
51+
}
52+
53+
/**
54+
* Authenticate to dock.mau.dev.
55+
* @param image
56+
* @param requestOptions
57+
* @returns {Promise<*>}
58+
*/
59+
async authenticate(image, requestOptions) {
60+
const request = {
61+
method: 'GET',
62+
url: `${this.configuration.authurl}/jwt/auth?service=container_registry&scope=repository:${image.name}:pull`,
63+
headers: {
64+
Accept: 'application/json',
65+
},
66+
};
67+
68+
if (this.configuration.token) {
69+
request.headers.Authorization = `Basic ${Mau.base64Encode('', this.configuration.token)}`;
70+
}
71+
72+
const response = await axios(request);
73+
const requestOptionsWithAuth = requestOptions;
74+
requestOptionsWithAuth.headers.Authorization = `Bearer ${response.data.token}`;
75+
return requestOptionsWithAuth;
76+
}
77+
78+
/**
79+
* Return auth for pull when token is configured.
80+
* @returns {{password: *, username: string}|undefined}
81+
*/
82+
async getAuthPull() {
83+
if (!this.configuration.token) {
84+
return undefined;
85+
}
86+
return {
87+
username: '',
88+
password: this.configuration.token,
89+
};
90+
}
91+
}
92+
93+
export default Mau;

app/registry/index.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ test('registerRegistries should register all registries', async () => {
158158
'hub.private',
159159
'ibmcr.public',
160160
'lscr.public',
161+
'mau.public',
161162
'ocir.public',
162163
'quay.public',
163164
'trueforge.public',
@@ -178,6 +179,7 @@ test('registerRegistries should register all anonymous registries by default', a
178179
'hub.public',
179180
'ibmcr.public',
180181
'lscr.public',
182+
'mau.public',
181183
'ocir.public',
182184
'quay.public',
183185
'trueforge.public',
@@ -563,6 +565,7 @@ test('init should register all components', async () => {
563565
'hub.private',
564566
'ibmcr.public',
565567
'lscr.public',
568+
'mau.public',
566569
'ocir.public',
567570
'quay.public',
568571
'trueforge.public',

app/registry/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ async function registerRegistries() {
345345
hub: { public: '' },
346346
ibmcr: { public: '' },
347347
lscr: { public: '' },
348+
mau: { public: '' },
348349
ocir: { public: '' },
349350
quay: { public: '' },
350351
trueforge: { public: '' },

0 commit comments

Comments
 (0)