Skip to content

Commit 16240a3

Browse files
Copilotneilime
andcommitted
refactor: share docker setup and registry auth
Co-authored-by: neilime <314088+neilime@users.noreply.github.com>
1 parent 5ba35e3 commit 16240a3

5 files changed

Lines changed: 456 additions & 412 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ _Actions that operate on OCI images across their build, metadata, and lifecycle
3434

3535
#### - [Prune pull requests image tags](actions/docker/prune-pull-requests-image-tags/README.md)
3636

37+
#### - [Setup](actions/docker/setup/README.md)
38+
3739
#### - [Sign images](actions/docker/sign-images/README.md)
3840

3941
### Helm

actions/docker/build-image/action.yml

Lines changed: 10 additions & 244 deletions
Original file line numberDiff line numberDiff line change
@@ -146,205 +146,19 @@ runs:
146146
with:
147147
value: ${{ inputs.platform }}
148148

149-
- id: resolve-oci-registries
150-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
149+
- id: docker-setup
150+
uses: ./self-actions/docker/setup
151151
with:
152-
script: |
153-
function parseJsonObjectInput(inputName, rawValue) {
154-
const value = `${rawValue}`.trim();
155-
if (!value.length) {
156-
return null;
157-
}
158-
159-
if (!value.startsWith('{')) {
160-
return value;
161-
}
162-
163-
let parsedValue;
164-
try {
165-
parsedValue = JSON.parse(value);
166-
} catch (error) {
167-
throw new Error(`"${inputName}" input is not a valid JSON object: ${error}`);
168-
}
169-
170-
if (parsedValue === null || Array.isArray(parsedValue) || typeof parsedValue !== 'object') {
171-
throw new Error(`"${inputName}" input must be a string or a JSON object`);
172-
}
173-
174-
return parsedValue;
175-
}
176-
177-
function normalizeString(value, fieldName) {
178-
if (typeof value !== 'string') {
179-
throw new Error(`"${fieldName}" must be a string`);
180-
}
181-
182-
const trimmedValue = value.trim();
183-
if (!trimmedValue.length) {
184-
throw new Error(`"${fieldName}" must not be empty`);
185-
}
186-
187-
return trimmedValue;
188-
}
189-
190-
function isPullRole(role) {
191-
return role === 'pull' || role.startsWith('pull:');
192-
}
193-
194-
function normalizeRoleMapInput(inputName, rawValue) {
195-
const parsedValue = parseJsonObjectInput(inputName, rawValue);
196-
197-
if (parsedValue === null) {
198-
return {};
199-
}
200-
201-
if (typeof parsedValue === 'string') {
202-
return { scalar: normalizeString(parsedValue, inputName) };
203-
}
204-
205-
return Object.entries(parsedValue).reduce((roleMap, [key, value]) => {
206-
if (key !== 'push' && key !== 'cache' && !isPullRole(key)) {
207-
throw new Error(`"${inputName}.${key}" is not supported`);
208-
}
209-
210-
roleMap[key] = normalizeString(value, `${inputName}.${key}`);
211-
return roleMap;
212-
}, {});
213-
}
214-
215-
function resolveCredentialByRole(credentialMap, role, registry, pushRegistry) {
216-
const defaultCredential = credentialMap.scalar ?? '';
217-
218-
if (role === 'push') {
219-
return credentialMap.push ?? defaultCredential;
220-
}
221-
222-
if (role === 'cache') {
223-
return credentialMap.cache ?? credentialMap.push ?? defaultCredential;
224-
}
225-
226-
if (!isPullRole(role)) {
227-
return defaultCredential;
228-
}
229-
230-
if (credentialMap[role] !== undefined) {
231-
return credentialMap[role];
232-
}
233-
234-
if (credentialMap.pull !== undefined) {
235-
return credentialMap.pull;
236-
}
237-
238-
if (registry === pushRegistry && credentialMap.push !== undefined) {
239-
return credentialMap.push;
240-
}
241-
242-
return defaultCredential;
243-
}
244-
245-
const registryInput = normalizeRoleMapInput('oci-registry', `${{ inputs.oci-registry }}`);
246-
247-
let pushRegistry = '';
248-
let cacheRegistry = '';
249-
let pullRegistryEntries = [];
250-
let pullRegistries = [];
251-
252-
if (registryInput.scalar) {
253-
pushRegistry = registryInput.scalar;
254-
cacheRegistry = pushRegistry;
255-
pullRegistries = [pushRegistry];
256-
} else {
257-
pushRegistry = registryInput.push ?? '';
258-
cacheRegistry = registryInput.cache ?? pushRegistry;
259-
pullRegistryEntries = Object.entries(registryInput)
260-
.filter(([key]) => isPullRole(key))
261-
.map(([role, registry]) => ({ role, registry }));
262-
263-
if (!pushRegistry.length) {
264-
throw new Error(`"oci-registry.push" is required when "oci-registry" uses the JSON object format`);
265-
}
266-
267-
if (!pullRegistryEntries.length) {
268-
pullRegistryEntries = [{ role: 'pull', registry: pushRegistry }];
269-
}
270-
271-
pullRegistries = pullRegistryEntries.map(({ registry }) => registry);
272-
}
273-
274-
const cacheType = `${{ inputs.cache-type }}`.trim();
275-
const registryEntries = [
276-
{ role: 'push', registry: pushRegistry, required: true },
277-
...pullRegistryEntries.map(pullRegistryEntry => ({ ...pullRegistryEntry, required: false })),
278-
];
279-
280-
if (cacheType === 'registry') {
281-
registryEntries.push({ role: 'cache', registry: cacheRegistry, required: true });
282-
}
283-
284-
const usernameByRole = normalizeRoleMapInput('oci-registry-username', `${{ inputs.oci-registry-username }}`);
285-
const passwordByRole = normalizeRoleMapInput('oci-registry-password', `${{ inputs.oci-registry-password }}`);
286-
287-
const registryLoginsByRegistry = new Map();
288-
for (const registryEntry of registryEntries) {
289-
const { role, registry, required } = registryEntry;
290-
const username = resolveCredentialByRole(usernameByRole, role, registry, pushRegistry);
291-
const password = resolveCredentialByRole(passwordByRole, role, registry, pushRegistry);
292-
293-
if ((username && !password) || (!username && password)) {
294-
throw new Error(`Credentials for "${role}" must define both username and password`);
295-
}
296-
297-
const existingRegistryLogin = registryLoginsByRegistry.get(registry);
298-
if (existingRegistryLogin) {
299-
const hasDifferentUsername = existingRegistryLogin.username && username && existingRegistryLogin.username !== username;
300-
const hasDifferentPassword = existingRegistryLogin.password && password && existingRegistryLogin.password !== password;
301-
if (hasDifferentUsername || hasDifferentPassword) {
302-
throw new Error(`Conflicting credentials configured for registry "${registry}"`);
303-
}
304-
305-
if (!existingRegistryLogin.username && username) {
306-
existingRegistryLogin.username = username;
307-
}
308-
309-
if (!existingRegistryLogin.password && password) {
310-
existingRegistryLogin.password = password;
311-
}
312-
existingRegistryLogin.required ||= required;
313-
continue;
314-
}
315-
316-
registryLoginsByRegistry.set(registry, {
317-
registry,
318-
username,
319-
password,
320-
required,
321-
});
322-
}
323-
324-
const registryLogins = [...registryLoginsByRegistry.values()].map(registryLogin => {
325-
if (registryLogin.required && (!registryLogin.username || !registryLogin.password)) {
326-
throw new Error(`Credentials for registry "${registryLogin.registry}" are required`);
327-
}
328-
329-
return registryLogin;
330-
});
331-
332-
const registryOutputNames = {
333-
push: ['push', 'registry'].join('-'),
334-
cache: ['cache', 'registry'].join('-'),
335-
pull: ['pull', 'registries'].join('-'),
336-
logins: ['registry', 'logins'].join('-'),
337-
};
338-
339-
core.setOutput(registryOutputNames.push, pushRegistry);
340-
core.setOutput(registryOutputNames.cache, cacheRegistry);
341-
core.setOutput(registryOutputNames.pull, JSON.stringify(pullRegistries));
342-
core.setOutput(registryOutputNames.logins, JSON.stringify(registryLogins));
152+
oci-registry: ${{ inputs.oci-registry }}
153+
oci-registry-username: ${{ inputs.oci-registry-username }}
154+
oci-registry-password: ${{ inputs.oci-registry-password }}
155+
cache-type: ${{ inputs.cache-type }}
156+
setup-docker: true
343157

344158
- id: metadata
345159
uses: ./self-actions/docker/get-image-metadata
346160
with:
347-
oci-registry: ${{ steps.resolve-oci-registries.outputs.push-registry }}
161+
oci-registry: ${{ steps.docker-setup.outputs.push-registry }}
348162
repository: ${{ inputs.repository }}
349163
image: ${{ inputs.image }}
350164
tag: ${{ inputs.tag }}
@@ -382,7 +196,7 @@ runs:
382196
383197
const cacheType = `${{ inputs.cache-type }}`.trim();
384198
const metadataImage = `${{ steps.metadata.outputs.image }}`;
385-
const cacheRegistry = `${{ steps.resolve-oci-registries.outputs.cache-registry }}`.trim();
199+
const cacheRegistry = `${{ steps.docker-setup.outputs.cache-registry }}`.trim();
386200
const metadataImageWithoutRegistry = metadataImage.replace(/^[^\/]+\//, '');
387201
const cacheBaseImage = cacheRegistry.length ? `${cacheRegistry}/${metadataImageWithoutRegistry}` : metadataImage;
388202
const cacheImage = cacheType === 'registry' ? `${cacheBaseImage}/cache` : metadataImage;
@@ -443,23 +257,11 @@ runs:
443257
}
444258
}
445259
446-
- if: steps.get-docker-config.outputs.docker-exists != 'true'
447-
uses: docker/setup-docker-action@1a6edb0ba9ac496f6850236981f15d8f9a82254d # v5.0.0
448-
449260
- if: steps.get-docker-config.outputs.platform-exists != 'true'
450261
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
451262
with:
452263
platforms: ${{ inputs.platform }}
453264

454-
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
455-
id: setup-buildx
456-
with:
457-
# FIXME: upgrade version when available (https://github.com/docker/buildx/releases)
458-
version: v0.31.1
459-
# FIXME: upgrade version when available (https://hub.docker.com/r/moby/buildkit)
460-
driver-opts: |
461-
image=moby/buildkit:v0.27.0
462-
463265
# Caching setup
464266
- id: cache-arguments
465267
uses: int128/docker-build-cache-config-action@3a4a4fababc091be29633e5a2b3bbf523996802a # v1.47.0
@@ -479,46 +281,10 @@ runs:
479281
- name: Restore Docker cache mounts
480282
uses: reproducible-containers/buildkit-cache-dance@1b8ab18fbda5ad3646e3fcc9ed9dd41ce2f297b4 # v3.3.2
481283
with:
482-
builder: ${{ steps.setup-buildx.outputs.name }}
284+
builder: ${{ steps.docker-setup.outputs.buildx-name }}
483285
cache-dir: cache-mount
484286
dockerfile: ${{ steps.get-docker-config.outputs.dockerfile-path }}
485287
skip-extraction: ${{ steps.cache.outputs.cache-hit }}
486-
487-
- id: login-oci-registries
488-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
489-
with:
490-
script: |
491-
const registryLoginsInput = `${{ steps.resolve-oci-registries.outputs.registry-logins }}`;
492-
let registryLogins = [];
493-
try {
494-
registryLogins = JSON.parse(registryLoginsInput);
495-
} catch (error) {
496-
throw new Error(`Resolved registry logins are not a valid JSON array: ${error}`);
497-
}
498-
499-
for (const registryLogin of registryLogins) {
500-
const { registry, username, password, required } = registryLogin;
501-
502-
if (!username && !password) {
503-
if (required) {
504-
throw new Error(`Credentials for registry "${registry}" are required`);
505-
}
506-
507-
core.info(`Skipping Docker login for optional registry "${registry}" because no credentials were provided.`);
508-
continue;
509-
}
510-
511-
await exec.exec(
512-
'docker',
513-
['login', registry, '--username', username, '--password-stdin'],
514-
{
515-
input: `${password}\n`,
516-
silent: true,
517-
},
518-
);
519-
520-
core.info(`Logged in to "${registry}".`);
521-
}
522288
# jscpd:ignore-end
523289
- id: build
524290
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0

0 commit comments

Comments
 (0)