@adobe/aio-lib-cloudmanager
Advanced tools
Comparing version 1.0.1 to 1.1.0
# Changelog | ||
# [1.1.0](https://github.com/adobe/aio-lib-cloudmanager/compare/1.0.1...1.1.0) (2021-06-30) | ||
### Features | ||
* **execution:** add tailExecutionStepLog method. fixes [#170](https://github.com/adobe/aio-lib-cloudmanager/issues/170) ([#171](https://github.com/adobe/aio-lib-cloudmanager/issues/171)) ([36dbcb0](https://github.com/adobe/aio-lib-cloudmanager/commit/36dbcb05fea4bf9ca40cdcd35d6ebe3185e250fc)) | ||
## [1.0.1](https://github.com/adobe/aio-lib-cloudmanager/compare/1.0.0...1.0.1) (2021-06-30) | ||
@@ -4,0 +11,0 @@ |
{ | ||
"name": "@adobe/aio-lib-cloudmanager", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "Adobe I/O Cloud Manager Library", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -159,2 +159,3 @@ <!-- | ||
* [.getExecutionStepLog(programId, pipelineId, executionId, action, logFile, outputStream)](#CloudManagerAPI+getExecutionStepLog) ⇒ <code>Promise.<object></code> | ||
* [.tailExecutionStepLog(programId, pipelineId, action, logFile, outputStream)](#CloudManagerAPI+tailExecutionStepLog) ⇒ [<code>Promise.<PipelineExecutionStepState></code>](#PipelineExecutionStepState) | ||
* [.listAvailableLogOptions(programId, environmentId)](#CloudManagerAPI+listAvailableLogOptions) ⇒ <code>Promise.<Array.<LogOptionRepresentation>></code> | ||
@@ -376,2 +377,18 @@ * [.downloadLogs(programId, environmentId, service, name, days, outputDirectory)](#CloudManagerAPI+downloadLogs) ⇒ <code>Promise.<Array.<DownloadedLog>></code> | ||
<a name="CloudManagerAPI+tailExecutionStepLog"></a> | ||
### cloudManagerAPI.tailExecutionStepLog(programId, pipelineId, action, logFile, outputStream) ⇒ [<code>Promise.<PipelineExecutionStepState></code>](#PipelineExecutionStepState) | ||
Tail step log to an output stream. | ||
**Kind**: instance method of [<code>CloudManagerAPI</code>](#CloudManagerAPI) | ||
**Returns**: [<code>Promise.<PipelineExecutionStepState></code>](#PipelineExecutionStepState) - the completed step state | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| programId | <code>string</code> | the program id | | ||
| pipelineId | <code>string</code> | the pipeline id | | ||
| action | <code>string</code> | the action | | ||
| logFile | <code>string</code> | the log file to select a non-default value | | ||
| outputStream | <code>object</code> | the output stream to write to | | ||
<a name="CloudManagerAPI+listAvailableLogOptions"></a> | ||
@@ -378,0 +395,0 @@ |
@@ -31,3 +31,3 @@ /* | ||
PipelineStepMetrics, Environment, LogOptionRepresentation, | ||
DownloadedLog, PipelineUpdate, Variable, IPAllowedList */ // for linter | ||
DownloadedLog, PipelineUpdate, Variable, IPAllowedList, PipelineExecutionStepState */ // for linter | ||
@@ -609,2 +609,80 @@ /** | ||
async _refreshStepState (href) { | ||
return this._get(href, codes.ERROR_REFRESH_STEP_STATE).then(res => { | ||
return res.json() | ||
}, e => { | ||
throw e | ||
}) | ||
} | ||
async _getExecutionStepLogUrl (href) { | ||
return this._get(href, codes.ERROR_GET_LOG).then(async (res) => { | ||
const json = await res.json() | ||
if (json.redirect) { | ||
return json.redirect | ||
} else { | ||
throw new codes.ERROR_NO_LOG_REDIRECT({ messageValues: [res.url, JSON.stringify(json)] }) | ||
} | ||
}, e => { | ||
throw e | ||
}) | ||
} | ||
/** | ||
* Tail step log to an output stream. | ||
* | ||
* @param {string} programId the program id | ||
* @param {string} pipelineId the pipeline id | ||
* @param {string} action the action | ||
* @param {string} logFile the log file to select a non-default value | ||
* @param {object} outputStream the output stream to write to | ||
* @returns {Promise<PipelineExecutionStepState>} the completed step state | ||
*/ | ||
async tailExecutionStepLog (programId, pipelineId, action, logFile, outputStream) { | ||
if (!outputStream) { | ||
outputStream = logFile | ||
logFile = undefined | ||
} | ||
const currentExecution = halfred.parse(await this.getCurrentExecution(programId, pipelineId)) | ||
let stepState = findStepState(currentExecution, action) | ||
if (!stepState) { | ||
throw new codes.ERROR_FIND_STEP_STATE({ messageValues: [action, currentExecution.id] }) | ||
} | ||
if (stepState.status === 'RUNNING') { | ||
const selfStateHref = stepState.link(rels.self).href | ||
let link = stepState.link(rels.stepLogs).href | ||
if (logFile) { | ||
link = `${link}?file=${logFile}` | ||
} | ||
const tailingSasUrl = await this._getExecutionStepLogUrl(link) | ||
let currentStartLimit = 0 | ||
while (stepState.status === 'RUNNING') { | ||
const options = { | ||
headers: { | ||
Range: `bytes=${currentStartLimit}-`, | ||
}, | ||
} | ||
const res = await fetch(tailingSasUrl, options) | ||
if (res.status === 206) { | ||
const contentLength = res.headers.get('content-length') | ||
await this._pipeBody(res.body, outputStream) | ||
currentStartLimit = parseInt(currentStartLimit) + parseInt(contentLength) | ||
console.log(currentStartLimit) | ||
} else if (res.status === 416 || res.status === 404) { | ||
// 416 means there's more data potentially available; 404 means the log isn't ready yet | ||
// these are different things, but we can treat them the same way -- wait a few seconds | ||
await sleep(5000) | ||
} | ||
stepState = await this._refreshStepState(selfStateHref) | ||
} | ||
} else { | ||
throw new codes.ERROR_STEP_STATE_NOT_RUNNING({ messageValues: [action, currentExecution.id] }) | ||
} | ||
return stepState | ||
} | ||
async _findEnvironment (programId, environmentId) { | ||
@@ -611,0 +689,0 @@ const environments = await this.listEnvironments(programId) |
@@ -105,1 +105,3 @@ /* | ||
E('ERROR_UNSUPPORTED_ADVANCE_STEP', 'Advancing the step %s is not supported in the CLI at present.') | ||
E('ERROR_REFRESH_STEP_STATE', 'Cannot refresh step state: %s') | ||
E('ERROR_STEP_STATE_NOT_RUNNING', 'The %s step in execution %s is not currently running.') |
@@ -19,13 +19,27 @@ /* | ||
let writable | ||
let written = '' | ||
let written | ||
let flushWritable | ||
let originalSetTimeout | ||
beforeEach(() => { | ||
written = '' | ||
const write = sinon.stub().callsFake((chunk, enc, callback) => callback()) | ||
writable = new Writable({ write }) | ||
writable.on('finish', () => { | ||
flushWritable = () => { | ||
written = write.args.map(args => args[0]).map(a => a.toString('utf8')).join('') | ||
}) | ||
} | ||
writable.on('finish', flushWritable) | ||
originalSetTimeout = global.setTimeout | ||
global.setTimeout = jest.fn().mockImplementation((cb) => cb()) | ||
}) | ||
afterEach(() => { | ||
global.setTimeout = originalSetTimeout | ||
}) | ||
test('getCurrentExecution - failure', async () => { | ||
@@ -482,1 +496,109 @@ expect.assertions(2) | ||
}) | ||
test('tailExecutionStepLog - failure no current execution', async () => { | ||
expect.assertions(3) | ||
const sdkClient = await createSdkClient() | ||
const result = sdkClient.tailExecutionStepLog('5', '5', 'build', writable) | ||
await expect(result instanceof Promise).toBeTruthy() | ||
await expect(result).rejects.toEqual( | ||
new codes.ERROR_GET_EXECUTION({ messageValues: 'https://cloudmanager.adobe.io/api/program/5/pipeline/5/execution (404 Not Found)' }), | ||
) | ||
expect(written).toEqual('') | ||
}) | ||
test('tailExecutionStepLog - step not running', async () => { | ||
fetchMock.setPipeline7Execution('1003') | ||
expect.assertions(3) | ||
const sdkClient = await createSdkClient() | ||
const result = sdkClient.tailExecutionStepLog('5', '7', 'build', writable) | ||
await expect(result instanceof Promise).toBeTruthy() | ||
await expect(result).rejects.toEqual( | ||
new codes.ERROR_STEP_STATE_NOT_RUNNING({ messageValues: ['build', '1003'] }), | ||
) | ||
expect(written).toEqual('') | ||
}) | ||
test('tailExecutionStepLog - bad step name', async () => { | ||
fetchMock.setPipeline7Execution('1003') | ||
expect.assertions(3) | ||
const sdkClient = await createSdkClient() | ||
const result = sdkClient.tailExecutionStepLog('5', '7', 'BUILD', writable) | ||
await expect(result instanceof Promise).toBeTruthy() | ||
await expect(result).rejects.toEqual( | ||
new codes.ERROR_FIND_STEP_STATE({ messageValues: ['BUILD', '1003'] }), | ||
) | ||
expect(written).toEqual('') | ||
}) | ||
test('tailExecutionStepLog - step log endpoint returns error', async () => { | ||
fetchMock.setPipeline7Execution('1017') | ||
expect.assertions(3) | ||
const sdkClient = await createSdkClient() | ||
const result = sdkClient.tailExecutionStepLog('5', '7', 'build', 'error', writable) | ||
await expect(result instanceof Promise).toBeTruthy() | ||
await expect(result).rejects.toEqual( | ||
new codes.ERROR_GET_LOG({ messageValues: ['https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1017/phase/4596/step/8492/logs?file=error', '(404 Not Found)'] }), | ||
) | ||
expect(written).toEqual('') | ||
}) | ||
test('tailExecutionStepLog - step log endpoint returns no redirect', async () => { | ||
fetchMock.setPipeline7Execution('1017') | ||
expect.assertions(3) | ||
const sdkClient = await createSdkClient() | ||
const result = sdkClient.tailExecutionStepLog('5', '7', 'build', 'noredirect', writable) | ||
await expect(result instanceof Promise).toBeTruthy() | ||
await expect(result).rejects.toEqual( | ||
new codes.ERROR_NO_LOG_REDIRECT({ messageValues: ['https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1017/phase/4596/step/8492/logs?file=noredirect', '{"garbage":"true"}'] }), | ||
) | ||
expect(written).toEqual('') | ||
}) | ||
test('tailExecutionStepLog - success', async () => { | ||
fetchMock.setPipeline7Execution('1017') | ||
expect.assertions(7) | ||
const sdkClient = await createSdkClient() | ||
const result = sdkClient.tailExecutionStepLog('5', '7', 'build', writable) | ||
await expect(result instanceof Promise).toBeTruthy() | ||
await expect(result).resolves.toBeTruthy() | ||
await expect(fetchMock.called('tail-step-log-1017-first')).toBe(true) | ||
await expect(fetchMock.called('tail-step-log-1017-second')).toBe(true) | ||
await expect(fetchMock.called('tail-step-log-1017-third')).toBe(true) | ||
await expect(fetchMock.calls('tail-step-log-1017-third').length).toEqual(2) | ||
flushWritable() | ||
expect(written).toEqual('some log message\nsome second log message\nsome third log message\n') | ||
}) | ||
test('tailExecutionStepLog - faling refresh', async () => { | ||
fetchMock.setPipeline7Execution('1018') | ||
expect.assertions(4) | ||
const sdkClient = await createSdkClient() | ||
const result = sdkClient.tailExecutionStepLog('5', '7', 'build', writable) | ||
await expect(result instanceof Promise).toBeTruthy() | ||
await expect(result).rejects.toEqual( | ||
new codes.ERROR_REFRESH_STEP_STATE({ messageValues: ['https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1018/phase/4596/step/8492', '(500 Internal Server Error)'] }), | ||
) | ||
await expect(fetchMock.called('tail-step-log-1018-first')).toBe(true) | ||
flushWritable() | ||
expect(written).toEqual('some log message\n') | ||
}) |
@@ -162,3 +162,3 @@ { | ||
}, | ||
"id": "1001", | ||
"id": "1003", | ||
"programId": "5", | ||
@@ -165,0 +165,0 @@ "pipelineId": "7", |
@@ -1417,2 +1417,3 @@ /* | ||
1001: require('./data/execution1001.json'), | ||
1003: require('./data/execution1003.json'), | ||
1005: require('./data/execution1005.json'), | ||
@@ -1430,2 +1431,4 @@ 1006: require('./data/execution1006.json'), | ||
1016: require('./data/execution1016.json'), | ||
1017: require('./data/execution1017.json'), | ||
1018: require('./data/execution1018.json'), | ||
} | ||
@@ -1493,3 +1496,100 @@ mockResponseWithMethod('https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution', 'GET', () => pipeline7Executions[executionForPipeline7]) | ||
}) | ||
mockResponseWithMethod('https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1017/phase/4596/step/8492/logs?file=error', 'GET', 404) | ||
mockResponseWithMethod('https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1017/phase/4596/step/8492/logs?file=noredirect', 'GET', { | ||
garbage: 'true', | ||
}) | ||
mockResponseWithMethod('https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1017/phase/4596/step/8492/logs', 'GET', { | ||
redirect: 'https://filestore/for-tailing.txt', | ||
}) | ||
fetchMock.mock({ | ||
url: 'https://filestore/for-tailing.txt', | ||
headers: { range: 'bytes=0-' }, | ||
name: 'tail-step-log-1017-first', | ||
}, () => { | ||
const logResponse = new Readable() | ||
logResponse.push('some log message\n') | ||
logResponse.push(null) | ||
return { | ||
status: 206, | ||
headers: { | ||
'content-length': '1000', | ||
}, | ||
body: logResponse, | ||
} | ||
}, { sendAsJson: false }) | ||
fetchMock.mock({ | ||
url: 'https://filestore/for-tailing.txt', | ||
headers: { range: 'bytes=1000-' }, | ||
name: 'tail-step-log-1017-second', | ||
}, () => { | ||
const logResponse = new Readable() | ||
logResponse.push('some second log message\n') | ||
logResponse.push(null) | ||
return { | ||
status: 206, | ||
headers: { | ||
'content-length': '1000', | ||
}, | ||
body: logResponse, | ||
} | ||
}, { sendAsJson: false }) | ||
let execution1017StepLogCounter = 0 | ||
fetchMock.mock({ | ||
url: 'https://filestore/for-tailing.txt', | ||
headers: { range: 'bytes=2000-' }, | ||
name: 'tail-step-log-1017-third', | ||
}, () => { | ||
execution1017StepLogCounter++ | ||
if (execution1017StepLogCounter === 1) { | ||
return { | ||
status: 416, | ||
} | ||
} else { | ||
const logResponse = new Readable() | ||
logResponse.push('some third log message\n') | ||
logResponse.push(null) | ||
return { | ||
status: 206, | ||
headers: { | ||
'content-length': '1000', | ||
}, | ||
body: logResponse, | ||
} | ||
} | ||
}, { sendAsJson: false }) | ||
let execution1017StepCounter = 0 | ||
fetchMock.mock('https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1017/phase/4596/step/8492', () => { | ||
execution1017StepCounter++ | ||
if (execution1017StepCounter < 4) { | ||
return pipeline7Executions[1017]._embedded.stepStates[1] | ||
} else { | ||
const cloned = _.cloneDeep(pipeline7Executions[1017]._embedded.stepStates[1]) | ||
cloned.status = 'FINISHED' | ||
return cloned | ||
} | ||
}) | ||
mockResponseWithMethod('https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1018/phase/4596/step/8492/logs', 'GET', { | ||
redirect: 'https://filestore/for-tailing1018.txt', | ||
}) | ||
fetchMock.mock({ | ||
url: 'https://filestore/for-tailing1018.txt', | ||
headers: { range: 'bytes=0-' }, | ||
name: 'tail-step-log-1018-first', | ||
}, () => { | ||
const logResponse = new Readable() | ||
logResponse.push('some log message\n') | ||
logResponse.push(null) | ||
return { | ||
status: 206, | ||
headers: { | ||
'content-length': '1000', | ||
}, | ||
body: logResponse, | ||
} | ||
}, { sendAsJson: false }) | ||
fetchMock.mock('https://cloudmanager.adobe.io/api/program/5/pipeline/7/execution/1018/phase/4596/step/8492', 500) | ||
fetchMock.mock('https://cloudmanager.adobe.io/api/program/7', 404) | ||
@@ -1496,0 +1596,0 @@ fetchMock.mock('https://cloudmanager.adobe.io/api/program/4/environment/10/variables', 404) |
@@ -19,3 +19,3 @@ /* | ||
let writable | ||
let written = '' | ||
let written | ||
@@ -27,2 +27,3 @@ let flushWritable | ||
beforeEach(() => { | ||
written = '' | ||
const write = sinon.stub().callsFake((chunk, enc, callback) => callback()) | ||
@@ -29,0 +30,0 @@ writable = new Writable({ write }) |
@@ -146,2 +146,12 @@ /** | ||
/** | ||
* Tail step log to an output stream. | ||
* @param programId - the program id | ||
* @param pipelineId - the pipeline id | ||
* @param action - the action | ||
* @param logFile - the log file to select a non-default value | ||
* @param outputStream - the output stream to write to | ||
* @returns the completed step state | ||
*/ | ||
tailExecutionStepLog(programId: string, pipelineId: string, action: string, logFile: string, outputStream: any): Promise<PipelineExecutionStepState>; | ||
/** | ||
* List the log options available for an environment | ||
@@ -148,0 +158,0 @@ * @param programId - the program id |
1864949
76
10479
916
6