Skip to content

Commit

Permalink
Add retries
Browse files Browse the repository at this point in the history
Make it possible to retry failed operations
  • Loading branch information
jooooel committed Mar 20, 2023
1 parent 94ccc77 commit c438482
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 4 deletions.
8 changes: 8 additions & 0 deletions action.yml
Expand Up @@ -19,6 +19,14 @@ inputs:
description: 'Enables kubelogin for non-admin user scenario. Values: true or false'
default: false
required: false
retries:
description: 'Number of times to retry setting the context'
default: 0
required: false
retry-delay:
description: 'Time to wait (in ms) between retries'
default: 0
required: false
branding:
color: 'green'
runs:
Expand Down
46 changes: 46 additions & 0 deletions src/run.test.ts
Expand Up @@ -172,4 +172,50 @@ describe('Set context', () => {
expect(fs.chmodSync).toBeCalledWith(kubeconfigPath, '600')
expect(core.exportVariable).toBeCalledWith('KUBECONFIG', kubeconfigPath)
})

it('retries getting the kubeconfig on exception', async () => {
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
if (inputName == 'resource-group') return resourceGroup
if (inputName == 'cluster-name') return clusterName
if (inputName == 'retries') return '1'
if (inputName == 'retry-delay') return '0'
})
jest.spyOn(io, 'which').mockImplementation(async () => azPath)
process.env['RUNNER_TEMP'] = runnerTemp
jest.spyOn(Date, 'now').mockImplementation(() => date)
jest
.spyOn(exec, 'exec')
.mockImplementationOnce(async () => {
throw new Error('asd')
})
.mockImplementationOnce(async () => 0)
jest.spyOn(fs, 'chmodSync').mockImplementation()
jest.spyOn(core, 'exportVariable').mockImplementation()
jest.spyOn(core, 'debug').mockImplementation()

await expect(run()).resolves.not.toThrowError()
expect(exec.exec).toBeCalledTimes(2)
})

it('retries getting the kubeconfig on unsuccessful return code', async () => {
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
if (inputName == 'resource-group') return resourceGroup
if (inputName == 'cluster-name') return clusterName
if (inputName == 'retries') return '1'
if (inputName == 'retry-delay') return '0'
})
jest.spyOn(io, 'which').mockImplementation(async () => azPath)
process.env['RUNNER_TEMP'] = runnerTemp
jest.spyOn(Date, 'now').mockImplementation(() => date)
jest
.spyOn(exec, 'exec')
.mockImplementationOnce(async () => 100)
.mockImplementationOnce(async () => 0)
jest.spyOn(fs, 'chmodSync').mockImplementation()
jest.spyOn(core, 'exportVariable').mockImplementation()
jest.spyOn(core, 'debug').mockImplementation()

await expect(run()).resolves.not.toThrowError()
expect(exec.exec).toBeCalledTimes(2)
})
})
59 changes: 55 additions & 4 deletions src/run.ts
Expand Up @@ -34,6 +34,8 @@ export async function run() {
const admin = adminInput.toLowerCase() === 'true'
const useKubeLoginInput = core.getInput('use-kubelogin') || ''
const useKubeLogin = useKubeLoginInput.toLowerCase() === 'true' && !admin
const retries = +core.getInput('retries') || 0
const retryDelay = +core.getInput('retry-delay') || 0

// check az tools
const azPath = await io.which(AZ_TOOL_NAME, false)
Expand Down Expand Up @@ -62,7 +64,11 @@ export async function run() {
if (subscription) cmd.push('--subscription', subscription)
if (admin) cmd.push('--admin')

const exitCode = await exec.exec(AZ_TOOL_NAME, cmd)
const exitCode = await retry(
() => exec.exec(AZ_TOOL_NAME, cmd),
retries,
retryDelay
)
if (exitCode !== 0)
throw Error('az cli exited with error code ' + exitCode)

Expand All @@ -76,9 +82,10 @@ export async function run() {
if (useKubeLogin) {
const kubeloginCmd = ['convert-kubeconfig', '-l', 'azurecli']

const kubeloginExitCode = await exec.exec(
KUBELOGIN_TOOL_NAME,
kubeloginCmd
const kubeloginExitCode = await retry(
() => exec.exec(KUBELOGIN_TOOL_NAME, kubeloginCmd),
retries,
retryDelay
)
if (kubeloginExitCode !== 0)
throw Error('kubelogin exited with error code ' + exitCode)
Expand All @@ -91,6 +98,50 @@ export async function run() {
}
}

async function retry(
action: () => Promise<number>,
retries: number,
retry_delay: number
): Promise<number> {
var exitCode: number
for (let attempt = 1; attempt <= retries + 1; attempt++) {
const retry = attempt <= retries

try {
exitCode = await action()

if (exitCode == 0 || !retry) {
// Success or no more retries, return exit code
return exitCode
}

core.warning('action failed with error code ' + exitCode)
} catch (error) {
if (retry) {
core.warning('action failed with error: ' + error)
} else {
throw error
}
}

core.info(
'attempt ' +
attempt +
' failed, retrying action in ' +
retry_delay +
'ms...'
)
await delay(retry_delay)
}

const errorMessage = 'az cli exited with error code ' + exitCode
throw Error(errorMessage)
}

async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

function getUserAgent(prevUserAgent: string): string {
const actionName = process.env.GITHUB_ACTION_REPOSITORY || ACTION_NAME
const runRepo = process.env.GITHUB_REPOSITORY || ''
Expand Down

0 comments on commit c438482

Please sign in to comment.