From 1590cb1ad13731695fb6cbebaa12327ecc107274 Mon Sep 17 00:00:00 2001 From: shaiic-pai Date: Fri, 18 Sep 2020 15:53:38 +0800 Subject: [PATCH] add addTag/deleteTag api, append tags filed to job detail/list --- docs/rest-api.md | 12 +++ src/api/v2/clients/jobClient.ts | 28 +++++++ src/api/v2/swagger.yaml | 120 ++++++++++++++++++++++++++++- tests/common/apiTestCases.ts | 52 +++++++++++++ tests/unit_tests/jobClient.spec.ts | 30 ++++++++ 5 files changed, 240 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 8d9ab28..4a85a47 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -207,6 +207,18 @@ const openPAIClient = new PAIV2.OpenPAIClient(cluster); await openPAIClient.job.updateJobExecutionType(username, jobname, 'STOP'); ``` +- [x] Add a tag to a job (PUT /api/v2/jobs/{username}~{jobname}/tag) + + ```ts + await openPAIClient.job.addTag(username, jobname, tag); + ``` + +- [x] Delete a tag from a job (DELETE /api/v2/jobs/{username}~{jobname}/tag) + + ```ts + await openPAIClient.job.deleteTag(username, jobname, tag); + ``` + ## job history - [x] Check if job attempts is healthy (GET /api/v2/jobs/{username}~{jobname}/job-attempts/healthz) diff --git a/src/api/v2/clients/jobClient.ts b/src/api/v2/clients/jobClient.ts index 02eba6c..ca885c2 100644 --- a/src/api/v2/clients/jobClient.ts +++ b/src/api/v2/clients/jobClient.ts @@ -94,4 +94,32 @@ export class JobClient extends OpenPAIBaseClient { ); return await this.httpClient.put(url, { value: type }); } + + /** + * Add a tag. + * @param userName The user name. + * @param jobName The job name. + * @param tag The tag. + */ + public async addTag(userName: string, jobName: string, tag: string): Promise { + const url: string = Util.fixUrl( + `${this.cluster.rest_server_uri}/api/v2/jobs/${userName}~${jobName}/tag`, + this.cluster.https + ); + return await this.httpClient.put(url, { value: tag }); + } + + /** + * Delelte a tag. + * @param userName The user name. + * @param jobName The job name. + * @param tag The tag. + */ + public async deleteTag(userName: string, jobName: string, tag: string): Promise { + const url: string = Util.fixUrl( + `${this.cluster.rest_server_uri}/api/v2/jobs/${userName}~${jobName}/tag`, + this.cluster.https + ); + return await this.httpClient.delete(url, undefined, { data: { value: tag } }); + } } diff --git a/src/api/v2/swagger.yaml b/src/api/v2/swagger.yaml index 2d69754..3e62a8f 100644 --- a/src/api/v2/swagger.yaml +++ b/src/api/v2/swagger.yaml @@ -5,13 +5,14 @@ info: Open Platform for AI RESTful API docs. Version 2.0.1: add more examples and fix typos Version 2.0.2: update job detail and job attempt schema - Version 2.0.3: update parameters description of get storage list + Version 2.0.3: update parameters description of get storage list, update storage example and add get job config example Version 2.0.4: add default field in get storage list Version 2.0.5: add more parameters to job list; add submissionTime + Version 2.1.0: add add/delete tag api; add tags field in get job detail and get job list; add tags filter in get job list license: name: MIT License url: "https://github.com/microsoft/pai/blob/master/LICENSE" - version: 2.0.5 + version: 2.1.0 externalDocs: description: Find out more about OpenPAI url: "https://github.com/microsoft/pai" @@ -1120,6 +1121,16 @@ paths: description: filter jobs with keyword, we search keyword in user name, job name, and virtual cluster name schema: type: string + - name: tagsContain + in: query + description: filter jobs with tags. When multiple tags are specified, every job selected should have at least one of these tags + schema: + type: string + - name: tagsNotContain + in: query + description: filter jobs with tags. When multiple tags are specified, every job selected should have none of these tags + schema: + type: string - name: offset in: query description: list job offset @@ -1156,6 +1167,7 @@ paths: state: SUCCEEDED subState: Completed executionType: STOP + tags: ['abnormal', 'low_gpu_utilization'] retries: 0 submissionTime: 0 createdTime: 0 @@ -1185,6 +1197,7 @@ paths: $ref: "#/components/schemas/JobDetail" example: name: job name + tags: ['abnormal', 'low_gpu_utilization'] jobStatus: username: user name state: SUCCEEDED @@ -1302,6 +1315,88 @@ paths: $ref: "#/components/responses/NoJobError" "500": $ref: "#/components/responses/UnknownError" + "/api/v2/jobs/{user}~{job}/tag": + put: + tags: + - job + summary: Add a tag to a job. + description: Add a tag to a job. + operationId: addTag + security: + - bearerAuth: [] + parameters: + - $ref: "#/components/parameters/user" + - $ref: "#/components/parameters/job" + requestBody: + content: + application/json: + schema: + type: object + properties: + value: + type: string + description: tag + required: + - value + required: true + responses: + "200": + description: Succeeded + content: + application/json: + schema: + $ref: "#/components/schemas/Response" + example: + message: "Add tag {tag} for job {job} successfully." + "404": + $ref: "#/components/responses/NoJobError" + "500": + $ref: "#/components/responses/UnknownError" + delete: + tags: + - job + summary: Delete a tag from a job. + description: Delete a tag from a job. + operationId: deleteTag + security: + - bearerAuth: [] + parameters: + - $ref: "#/components/parameters/user" + - $ref: "#/components/parameters/job" + requestBody: + content: + application/json: + schema: + type: object + properties: + value: + type: string + description: tag + required: + - value + required: true + responses: + "200": + description: Succeeded + content: + application/json: + schema: + $ref: "#/components/schemas/Response" + example: + message: "Delete tag {tag} from job {job} successfully." + "404": + description: NoJobError or NoTagError + content: + application/json: + schema: + $ref: "#/components/schemas/Response" + examples: + NoJobError: + $ref: "#/components/responses/NoJobError/content/application~1json/examples/NoJobError" + NoTagError: + $ref: "#/components/responses/NoTagError/content/application~1json/examples/NoTagError" + "500": + $ref: "#/components/responses/UnknownError" "/api/v2/jobs/{user}~{job}/job-attempts/healthz": get: tags: @@ -1674,6 +1769,11 @@ components: enum: - START - STOP + tags: + type: array + description: tags + items: + type: string retries: type: integer description: job retried times @@ -1738,6 +1838,11 @@ components: name: type: string description: job name + tags: + type: array + description: tags + items: + type: string jobStatus: type: object description: job status @@ -2565,6 +2670,17 @@ components: value: code: NoJobError message: "Job {job} is not found." + NoTagError: + description: NoTagError + content: + application/json: + schema: + $ref: "#/components/schemas/Response" + examples: + NoTagError: + value: + code: NoTagError + message: "Tag {tag} is not found for job {job} ." NoJobConfigError: description: NoJobConfigError content: diff --git a/tests/common/apiTestCases.ts b/tests/common/apiTestCases.ts index d9ec2ea..7b453d7 100644 --- a/tests/common/apiTestCases.ts +++ b/tests/common/apiTestCases.ts @@ -81,6 +81,44 @@ const deleteTestGroup: IApiOperation = { }] }; +const addTestTag: IApiOperation = { + tag: 'job', + operationId: 'addTag', + parameters: [ + { + type: 'raw', + value: clustersJson[0].username + }, + { + type: 'raw', + value: 'sdk_test_job' + randomString.get() + }, + { + type: 'raw', + value: 'testTag' + } + ] +}; + +const deleteTestTag: IApiOperation = { + tag: 'job', + operationId: 'deleteTag', + parameters: [ + { + type: 'raw', + value: clustersJson[0].username + }, + { + type: 'raw', + value: 'sdk_test_job' + randomString.get() + }, + { + type: 'raw', + value: 'testTag' + } + ] +}; + function createTestJob(): IApiOperation { return { tag: 'job', @@ -898,6 +936,20 @@ export const ApiDefaultTestCases: {[key: string]: IApiTestCase} = { ], after: [ updateTestJobExecutionType('STOP') ] }, + 'put /api/v2/jobs/{user}~{job}/tag': { + before: [ createTestJob() ], + tests: [{ + operation: addTestTag + }], + after: [ updateTestJobExecutionType('STOP'), deleteTestTag ] + }, + 'delete /api/v2/jobs/{user}~{job}/tag': { + before: [ createTestJob(), addTestTag ], + tests: [{ + operation: deleteTestTag + }], + after: [ updateTestJobExecutionType('STOP') ] + }, 'get /api/v2/jobs/{user}~{job}/job-attempts': { before: [ createTestJob() ], tests: [{ diff --git a/tests/unit_tests/jobClient.spec.ts b/tests/unit_tests/jobClient.spec.ts index 7be8f19..dba404c 100644 --- a/tests/unit_tests/jobClient.spec.ts +++ b/tests/unit_tests/jobClient.spec.ts @@ -156,3 +156,33 @@ describe('Stop a job', () => { expect(result).to.be.eql(response); }); }); + +describe('Add a tag', () => { + const response: any = { + message: 'Add tag testTag for job tensorflow_serving_mnist_2019_6585ba19 successfully.' + }; + const userName: string = 'core'; + const jobName: string = 'tensorflow_serving_mnist_2019_6585ba19'; + before(() => nock(`http://${testUri}`).put(`/api/v2/jobs/${userName}~${jobName}/tag`).reply(200, response)); + + it('should add a tag', async () => { + const jobClient: JobClient = new JobClient(cluster); + const result: any = await jobClient.addTag(userName, jobName, 'testTag'); + expect(result).to.be.eql(response); + }); +}); + +describe('Delete a tag', () => { + const response: any = { + message: 'Delete tag testTag from job tensorflow_serving_mnist_2019_6585ba19 successfully.' + }; + const userName: string = 'core'; + const jobName: string = 'tensorflow_serving_mnist_2019_6585ba19'; + before(() => nock(`http://${testUri}`).delete(`/api/v2/jobs/${userName}~${jobName}/tag`).reply(200, response)); + + it('should delete a tag', async () => { + const jobClient: JobClient = new JobClient(cluster); + const result: any = await jobClient.deleteTag(userName, jobName, 'testTag'); + expect(result).to.be.eql(response); + }); +});