mirror of
				https://github.com/docker/login-action.git
				synced 2025-10-31 10:10:09 +08:00 
			
		
		
		
	Merge 810869f3bb into 327cd5a69d
				
					
				
			This commit is contained in:
		
						commit
						9dbb803b69
					
				
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @ -500,13 +500,16 @@ jobs: | |||||||
| 
 | 
 | ||||||
| The following inputs can be used as `step.with` keys: | The following inputs can be used as `step.with` keys: | ||||||
| 
 | 
 | ||||||
| | Name       | Type   | Default | Description                                                                   | | | Name                  | Type   | Default | Description                                                                   | | ||||||
| |------------|--------|---------|-------------------------------------------------------------------------------| | |-----------------------|--------|---------|-------------------------------------------------------------------------------| | ||||||
| | `registry` | String |         | Server address of Docker registry. If not set then will default to Docker Hub | | | `registry`            | String |         | Server address of Docker registry. If not set then will default to Docker Hub | | ||||||
| | `username` | String |         | Username for authenticating to the Docker registry                            | | | `username`            | String |         | Username for authenticating to the Docker registry                            | | ||||||
| | `password` | String |         | Password or personal access token for authenticating the Docker registry      | | | `password`            | String |         | Password or personal access token for authenticating the Docker registry      | | ||||||
| | `ecr`      | String | `auto`  | Specifies whether the given registry is ECR (`auto`, `true` or `false`)       | | | `ecr`                 | String | `auto`  | Specifies whether the given registry is ECR (`auto`, `true` or `false`)       | | ||||||
| | `logout`   | Bool   | `true`  | Log out from the Docker registry at the end of a job                          | | | `logout`              | Bool   | `true`  | Log out from the Docker registry at the end of a job                          | | ||||||
|  | | `http-codes-to-retry` | String | `408,500,502,504` | Comma separated list of HTTP error codes we want to retry           | | ||||||
|  | | `max-attempts`        | String | `1`     | Overall maximum number of attempts we could make (`1` means no retries)       | | ||||||
|  | | `retry-timeout`       | String | `15`    | Timeout between retries, in seconds                                           | | ||||||
| 
 | 
 | ||||||
| ## Contributing | ## Contributing | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								__tests__/retries_fail.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								__tests__/retries_fail.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | import {expect, jest, test} from '@jest/globals'; | ||||||
|  | 
 | ||||||
|  | import {login} from '../src/docker'; | ||||||
|  | import {Docker} from '@docker/actions-toolkit/lib/docker/docker'; | ||||||
|  | 
 | ||||||
|  | test('login retries function', async () => { | ||||||
|  |   let stderrStrings: string[] = []; | ||||||
|  |   let callCount: number = -1; | ||||||
|  | 
 | ||||||
|  |   // using spyOn() here isn't enough, as we alter the logic
 | ||||||
|  |   // so use `jest.fn()` here for the `Docker.getExecOutput`
 | ||||||
|  |   Docker.getExecOutput = jest.fn(async () => { | ||||||
|  |     callCount++; | ||||||
|  |     console.log(`Mock: ${callCount}, ${stderrStrings}`); | ||||||
|  |     if (callCount >= stderrStrings.length) { | ||||||
|  |       return { | ||||||
|  |         exitCode: 0, | ||||||
|  |         stdout: 'Mock success', | ||||||
|  |         stderr: '' | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       exitCode: 1, | ||||||
|  |       stdout: '', | ||||||
|  |       stderr: stderrStrings[callCount % stderrStrings.length] | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   const username = 'dbowie'; | ||||||
|  |   const password = 'groundcontrol'; | ||||||
|  |   const registry = 'https://ghcr.io'; | ||||||
|  | 
 | ||||||
|  |   stderrStrings = ['mock error, failed with status: 408 Request Timeout', 'mock error, failed with status: 502 Request Timeout', 'mock error, failed with status: 400 Request Timeout']; | ||||||
|  |   callCount = -1; | ||||||
|  |   await expect(async () => { | ||||||
|  |     await login(registry, username, password, 'false', ['408', '400'], 5, 0.1); | ||||||
|  |   }).rejects.toThrow('mock error, failed with status: 502 Request Timeout'); | ||||||
|  |   expect(Docker.getExecOutput).toHaveBeenCalledTimes(2); | ||||||
|  | 
 | ||||||
|  |   stderrStrings = ['not matching error', 'mock error, failed with status: 502 Request Timeout', 'mock error, failed with status: 400 Request Timeout']; | ||||||
|  |   callCount = -1; | ||||||
|  |   await expect(async () => { | ||||||
|  |     await login(registry, username, password, 'false', ['408', '400'], 5, 0.1); | ||||||
|  |   }).rejects.toThrow('not matching error'); | ||||||
|  |   expect(Docker.getExecOutput).toHaveBeenCalledTimes(2 + 1); | ||||||
|  | }); | ||||||
							
								
								
									
										42
									
								
								__tests__/retries_success.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								__tests__/retries_success.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | import {expect, jest, test} from '@jest/globals'; | ||||||
|  | 
 | ||||||
|  | import {login} from '../src/docker'; | ||||||
|  | import {Docker} from '@docker/actions-toolkit/lib/docker/docker'; | ||||||
|  | 
 | ||||||
|  | test('login retries success function', async () => { | ||||||
|  |   let stderrStrings: string[] = []; | ||||||
|  |   let callCount: number = -1; | ||||||
|  | 
 | ||||||
|  |   // using spyOn() here isn't enough, as we alter the logic
 | ||||||
|  |   // so use `jest.fn()` here for the `Docker.getExecOutput`
 | ||||||
|  |   Docker.getExecOutput = jest.fn(async () => { | ||||||
|  |     callCount++; | ||||||
|  |     console.log(`Mock: ${callCount}, ${stderrStrings}`); | ||||||
|  |     if (callCount >= stderrStrings.length) { | ||||||
|  |       return { | ||||||
|  |         exitCode: 0, | ||||||
|  |         stdout: 'Mock success', | ||||||
|  |         stderr: '' | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       exitCode: 1, | ||||||
|  |       stdout: '', | ||||||
|  |       stderr: stderrStrings[callCount % stderrStrings.length] | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   const username = 'dbowie'; | ||||||
|  |   const password = 'groundcontrol'; | ||||||
|  |   const registry = 'https://ghcr.io'; | ||||||
|  | 
 | ||||||
|  |   stderrStrings = []; | ||||||
|  |   callCount = -1; | ||||||
|  |   await login(registry, username, password, 'false', ['408', '502', '400'], 5, 0.1); | ||||||
|  |   expect(Docker.getExecOutput).toHaveBeenCalledTimes(1); | ||||||
|  | 
 | ||||||
|  |   stderrStrings = ['mock error, failed with status: 408 Request Timeout', 'mock error, failed with status: 502 Request Timeout', 'mock error, failed with status: 400 Request Timeout']; | ||||||
|  |   callCount = -1; | ||||||
|  |   await login(registry, username, password, 'false', ['408', '502', '400'], 5, 0.1); | ||||||
|  |   expect(Docker.getExecOutput).toHaveBeenCalledTimes(1 + 4); | ||||||
|  | }); | ||||||
							
								
								
									
										11
									
								
								action.yml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								action.yml
									
									
									
									
									
								
							| @ -24,6 +24,17 @@ inputs: | |||||||
|     description: 'Log out from the Docker registry at the end of a job' |     description: 'Log out from the Docker registry at the end of a job' | ||||||
|     default: 'true' |     default: 'true' | ||||||
|     required: false |     required: false | ||||||
|  |   http-codes-to-retry: | ||||||
|  |     description: 'Comma separated list of HTTP error codes we want to retry' | ||||||
|  |     default: '408,500,502,504' | ||||||
|  |   max-attempts: | ||||||
|  |     description: 'Overall maximum number of attempts we will make trying to login' | ||||||
|  |     default: '1' | ||||||
|  |     required: false | ||||||
|  |   retry-timeout: | ||||||
|  |     description: 'Timeout between retries, in seconds' | ||||||
|  |     default: '15' | ||||||
|  |     required: false | ||||||
| 
 | 
 | ||||||
| runs: | runs: | ||||||
|   using: 'node20' |   using: 'node20' | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								dist/index.js
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/index.js
									
									
									
										generated
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								dist/index.js.map
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/index.js.map
									
									
									
										generated
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -6,6 +6,9 @@ export interface Inputs { | |||||||
|   password: string; |   password: string; | ||||||
|   ecr: string; |   ecr: string; | ||||||
|   logout: boolean; |   logout: boolean; | ||||||
|  |   httpCodesToRetry: string[]; | ||||||
|  |   maxAttempts: number; | ||||||
|  |   retryTimeout: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getInputs(): Inputs { | export function getInputs(): Inputs { | ||||||
| @ -14,6 +17,9 @@ export function getInputs(): Inputs { | |||||||
|     username: core.getInput('username'), |     username: core.getInput('username'), | ||||||
|     password: core.getInput('password'), |     password: core.getInput('password'), | ||||||
|     ecr: core.getInput('ecr'), |     ecr: core.getInput('ecr'), | ||||||
|     logout: core.getBooleanInput('logout') |     logout: core.getBooleanInput('logout'), | ||||||
|  |     httpCodesToRetry: core.getInput('http-codes-to-retry').split(','), | ||||||
|  |     maxAttempts: Number.parseInt(core.getInput('max-attempts')), | ||||||
|  |     retryTimeout: Number.parseInt(core.getInput('retry-timeout')) | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,11 +3,24 @@ import * as core from '@actions/core'; | |||||||
| 
 | 
 | ||||||
| import {Docker} from '@docker/actions-toolkit/lib/docker/docker'; | import {Docker} from '@docker/actions-toolkit/lib/docker/docker'; | ||||||
| 
 | 
 | ||||||
| export async function login(registry: string, username: string, password: string, ecr: string): Promise<void> { | export async function login(registry: string, username: string, password: string, ecr: string, httpCodesToRetry: string[], maxAttempts: number, retryTimeout: number): Promise<void> { | ||||||
|   if (/true/i.test(ecr) || (ecr == 'auto' && aws.isECR(registry))) { |   let succeeded: boolean = false; | ||||||
|     await loginECR(registry, username, password); |   for (let attempt = 1; attempt <= maxAttempts && !succeeded; attempt++) { | ||||||
|   } else { |     try { | ||||||
|     await loginStandard(registry, username, password); |       if (/true/i.test(ecr) || (ecr == 'auto' && aws.isECR(registry))) { | ||||||
|  |         await loginECR(registry, username, password); | ||||||
|  |       } else { | ||||||
|  |         await loginStandard(registry, username, password); | ||||||
|  |       } | ||||||
|  |       succeeded = true; | ||||||
|  |     } catch (error) { | ||||||
|  |       if (attempt < maxAttempts && isRetriableError(error.message, httpCodesToRetry)) { | ||||||
|  |         core.info(`Attempt ${attempt} out of ${maxAttempts} failed, retrying after ${retryTimeout} seconds`); | ||||||
|  |         await new Promise(r => setTimeout(r, retryTimeout * 1000)); | ||||||
|  |       } else { | ||||||
|  |         throw error; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -21,6 +34,17 @@ export async function logout(registry: string): Promise<void> { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function isRetriableError(errorMessage: string, httpCodesToRetry: string[]): boolean { | ||||||
|  |   for (const errCode of httpCodesToRetry) { | ||||||
|  |     if (errorMessage.includes('failed with status: ' + errCode)) { | ||||||
|  |       core.info(`Retryable match found in ${errorMessage} for retryable code: ${errCode}`); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   core.info(`No matches in ${errorMessage} when lookging for retryable codes: ${httpCodesToRetry}`); | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function loginStandard(registry: string, username: string, password: string): Promise<void> { | export async function loginStandard(registry: string, username: string, password: string): Promise<void> { | ||||||
|   if (!username && !password) { |   if (!username && !password) { | ||||||
|     throw new Error('Username and password required'); |     throw new Error('Username and password required'); | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ export async function main(): Promise<void> { | |||||||
|   const input: context.Inputs = context.getInputs(); |   const input: context.Inputs = context.getInputs(); | ||||||
|   stateHelper.setRegistry(input.registry); |   stateHelper.setRegistry(input.registry); | ||||||
|   stateHelper.setLogout(input.logout); |   stateHelper.setLogout(input.logout); | ||||||
|   await docker.login(input.registry, input.username, input.password, input.ecr); |   await docker.login(input.registry, input.username, input.password, input.ecr, input.httpCodesToRetry, input.maxAttempts, input.retryTimeout); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function post(): Promise<void> { | async function post(): Promise<void> { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Fedor Dikarev
						Fedor Dikarev