diff --git a/lib/interface/cli/commands/root/create.cmd.js b/lib/interface/cli/commands/root/create.cmd.js index faf33b3ea..05f304461 100644 --- a/lib/interface/cli/commands/root/create.cmd.js +++ b/lib/interface/cli/commands/root/create.cmd.js @@ -9,7 +9,7 @@ const get = new Command({ root: true, command: 'create', description: 'Create a resource from a file or stdin', - usage: 'Supported resources: \n\t\'context\'\n\t\'pipeline\'', + usage: 'Supported resources: \n\t\'context\'\n\t\'pipeline\'\'\n\t\'step\'', webDocs: { description: 'Create a resource from a file, directory or url', category: 'Operate On Resources', @@ -48,6 +48,10 @@ const get = new Command({ await sdk.pipelines.create(data); console.log(`Pipeline '${name}' created`); break; + case 'step': + await sdk.steps.create(data); + console.log(`Step '${name}' created`); + break; default: throw new CFError(`Entity: ${entity} not supported`); } diff --git a/lib/interface/cli/commands/root/replace.cmd.js b/lib/interface/cli/commands/root/replace.cmd.js index b3ca63391..69a54c89b 100644 --- a/lib/interface/cli/commands/root/replace.cmd.js +++ b/lib/interface/cli/commands/root/replace.cmd.js @@ -9,7 +9,7 @@ const annotate = new Command({ root: true, command: 'replace', description: 'Replace a resource by filename', - usage: 'Supported resources: \n\t\'Context\'\n\t\'Pipeline\'', + usage: 'Supported resources: \n\t\'Context\'\n\t\'Pipeline\'\'\n\t\'Step\'', webDocs: { description: 'Replace a resource from a file, directory or url', category: 'Operate On Resources', @@ -56,6 +56,12 @@ const annotate = new Command({ }, data); console.log(`Pipeline '${name}' updated`); break; + case 'step': + await sdk.steps.replace({ + name, + }, data); + console.log(`Step '${name}' updated`); + break; default: throw new CFError(`Entity: ${entity} not supported`); } diff --git a/lib/interface/cli/commands/step/delete.cmd.js b/lib/interface/cli/commands/step/delete.cmd.js new file mode 100644 index 000000000..f956aa23c --- /dev/null +++ b/lib/interface/cli/commands/step/delete.cmd.js @@ -0,0 +1,30 @@ +const Command = require('../../Command'); +const deleteRoot = require('../root/delete.cmd'); +const { sdk } = require('../../../../logic'); + + +const command = new Command({ + command: 'step [name]', + parent: deleteRoot, + description: 'Delete a step', + webDocs: { + category: 'Steps', + title: 'Delete Step', + }, + builder: (yargs) => { + return yargs + .positional('name', { + describe: 'Step name', + }); + }, + handler: async (argv) => { + const { name } = argv; + + await sdk.steps.delete({ name }); + console.log(`Step '${name}' deleted.`); + }, +}); + + +module.exports = command; + diff --git a/lib/interface/cli/commands/step/get.cmd.js b/lib/interface/cli/commands/step/get.cmd.js new file mode 100644 index 000000000..84de61d6f --- /dev/null +++ b/lib/interface/cli/commands/step/get.cmd.js @@ -0,0 +1,84 @@ +const debug = require('debug')('codefresh:cli:get:step'); +const Command = require('../../Command'); +const CFError = require('cf-errors'); +const _ = require('lodash'); +const DEFAULTS = require('../../defaults'); +const { prepareKeyValueFromCLIEnvOption } = require('../../helpers/general'); +const Output = require('../../../../output/Output'); +const { sdk } = require('../../../../logic'); +const Step = require('../../../../logic/entities/Step'); + +const getRoot = require('../root/get.cmd'); + + +const command = new Command({ + command: 'steps [id..]', + aliases: ['step'], + parent: getRoot, + description: 'Get a specific step or an array of steps', + webDocs: { + category: 'Steps', + title: 'Get Step', + }, + builder: (yargs) => { + return yargs + .positional('id', { + describe: 'Step name/id', + }) + .option('name', { + describe: 'Filter steps by name', + }) + .option('label', { + describe: 'Filter by a label', + alias: 'l', + default: [], + }) + .option('limit', { + describe: 'Limit amount of returned results', + default: DEFAULTS.GET_LIMIT_RESULTS, + }) + .option('page', { + describe: 'Paginated page', + default: DEFAULTS.GET_PAGINATED_PAGE, + }); + }, + handler: async (argv) => { + const { id: ids, name } = argv; + const limit = argv.limit; + const offset = (argv.page - 1) * limit; + const labels = prepareKeyValueFromCLIEnvOption(argv.label); + + if (!_.isEmpty(ids)) { + const steps = []; + for (const id of ids) { + try { + const currStep = await sdk.steps.get({ name: id }); + steps.push(Step.fromResponse(currStep)); + } catch (err) { + if (steps.length) { + Output.print(steps); + } + + debug(err.toString()); + const message = err.toString().includes('not find') ? `Step '${id}' was not found.` : 'Error occurred'; + throw new CFError({ + cause: err, + message, + }); + } + } + Output.print(steps); + } else { + const steps = await sdk.steps.list({ + limit, + offset, + id: name, + labels, + }); + Output.print(_.map(_.get(steps, 'docs'), Step.fromResponse)); + } + }, +}); + +module.exports = command; + diff --git a/lib/interface/cli/commands/step/get.completion.js b/lib/interface/cli/commands/step/get.completion.js new file mode 100644 index 000000000..84baa88ca --- /dev/null +++ b/lib/interface/cli/commands/step/get.completion.js @@ -0,0 +1,14 @@ +const _ = require('lodash'); +const steps = () => require('../../../../logic').sdk.steps; +const Step = require('../../../../logic/entities/Step'); +const { authContextWrapper } = require('../../completion/helpers'); + +const positionalHandler = async ({word, argv}) => { + const stps = await steps().list({ limit: 25, offset: 0 }); + return _.map(_.get(stps, 'docs'), Step.fromResponse).map(p => p.name); +}; + +module.exports = { + positionalHandler: authContextWrapper(positionalHandler), +}; + diff --git a/lib/interface/cli/commands/step/step.sdk.spec.js b/lib/interface/cli/commands/step/step.sdk.spec.js new file mode 100644 index 000000000..18521fe9c --- /dev/null +++ b/lib/interface/cli/commands/step/step.sdk.spec.js @@ -0,0 +1,67 @@ +const getCmd = require('./get.cmd').toCommand(); +const deleteCmd = require('./delete.cmd').toCommand(); +const { positionalHandler } = require('./get.completion'); + +jest.mock('../../helpers/validation'); // eslint-disable-line + +jest.mock('../../completion/helpers', () => { // eslint-disable-line + return { + authContextWrapper: func => func, + }; +}); + +jest.mock('../../../../logic/entities/Step', () => { // eslint-disable-line + return { + fromResponse: res => res, + }; +}); + +const request = require('requestretry'); + +const DEFAULT_RESPONSE = request.__defaultResponse(); + +describe('step', () => { + beforeEach(async () => { + request.__reset(); + request.mockClear(); + await configureSdk(); // eslint-disable-line + }); + + describe('commands', () => { + describe('get', () => { + it('should handle getting given id', async () => { + const argv = { id: ['some id'] }; + await getCmd.handler(argv); + await verifyResponsesReturned([DEFAULT_RESPONSE]); // eslint-disable-line + }); + + it('should handle getting all', async () => { + const argv = {}; + const response = { statusCode: 200, body: { docs: [DEFAULT_RESPONSE.body] } }; + request.__setResponse(response); + await getCmd.handler(argv); + await verifyResponsesReturned([response]); // eslint-disable-line + }); + }); + + // decided to follow unit tests modularity concept + describe('delete', () => { + it('should handle deletion given name', async () => { + const argv = { name: 'some name' }; + await deleteCmd.handler(argv); + await verifyResponsesReturned([DEFAULT_RESPONSE]); // eslint-disable-line + }); + }); + }); + + describe('completions', () => { + describe('get', () => { + it('should handle getting completion', async () => { + const response = { statusCode: 200, body: { docs: [{ name: 'some pip' }] } }; + request.__setResponse(response); + await positionalHandler({}); + await verifyResponsesReturned([response]); // eslint-disable-line + }); + }); + }); +}); diff --git a/lib/logic/entities/Step.js b/lib/logic/entities/Step.js new file mode 100644 index 000000000..b04f676ee --- /dev/null +++ b/lib/logic/entities/Step.js @@ -0,0 +1,22 @@ +const Entity = require('./Entity'); +const _ = require('lodash'); + +class Step extends Entity { + constructor(data) { + super(); + this.entityType = 'step'; + this.info = data; + this.id = this.info.metadata.id; + this.name = this.info.metadata.name; + this.created = this.info.metadata.created_at ? new Date(this.info.metadata.created_at) : undefined; + this.updated = this.info.metadata.updated_at ? new Date(this.info.metadata.updated_at) : undefined; + this.defaultColumns = ['name', 'updated', 'created']; + this.wideColumns = ['id'].concat(this.defaultColumns); + } + + static fromResponse(response) { + return new Step(_.pick(response, 'id', 'version', 'kind', 'metadata', 'spec')); + } +} + +module.exports = Step; diff --git a/openapi.json b/openapi.json index 9861de2ca..cece3c310 100644 --- a/openapi.json +++ b/openapi.json @@ -5394,6 +5394,372 @@ "x-sdk-interface": "pipelines.replace" } }, + + "/steps": { + "get": { + "responses": { + "200": { + "$ref": "#/components/responses/json" + } + }, + "tags": [ + "steps" + ], + "parameters": [ + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer" + }, + "description": "Offset" + }, + { + "name": "id", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Id" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "string" + }, + "description": "Limit" + }, + { + "in": "query", + "name": "labels", + "schema": { + "type": "string" + }, + "description": "Labels" + } + ], + "operationId": "steps-list", + "summary": "List", + "x-sdk-interface": "steps.list", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.getAllAccountSteps" + } + }, + "post": { + "x-entityId": { + "pathName": "body.metadata.name" + }, + "x-action": "stepCreated", + "responses": { + "200": { + "$ref": "#/components/responses/json" + } + }, + "tags": [ + "steps" + ], + "operationId": "steps-create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "summary": "Create", + "parameters": [], + "x-sdk-interface": "steps.create", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.createStep" + } + } + }, + "/steps/labels": { + "get": { + "responses": { + "200": { + "$ref": "#/components/responses/json" + } + }, + "tags": [ + "steps" + ], + "operationId": "steps-get-labels", + "parameters": [], + "summary": "Get all labels", + "x-sdk-interface": "steps.getLabels", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.getLabels" + } + } + }, + "/steps/labels/{label}": { + "get": { + "responses": { + "200": { + "$ref": "#/components/responses/json" + } + }, + "tags": [ + "steps" + ], + "operationId": "steps-get-label-values", + "parameters": [ + { + "in": "path", + "name": "label", + "schema": { + "type": "string" + }, + "required": true, + "description": "label" + } + + ], + "summary": "Get label values", + "x-sdk-interface": "steps.getLabelValues", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.getLabelValues" + } + } + }, + "/steps/names": { + "get": { + "parameters": [ + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer" + }, + "description": "Offset" + }, + { + "name": "id", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Id" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "string" + }, + "description": "Limit" + }, + { + "in": "query", + "name": "labels", + "schema": { + "type": "string" + }, + "description": "Labels" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/json" + } + }, + "tags": [ + "steps" + ], + "operationId": "steps-get-names", + "summary": "Get all step names", + "x-sdk-interface": "steps.getNames", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.getAllAccountStepsNames" + } + } + }, + "/steps/{name}": { + "get": { + "parameters": [ + { + "description": "Name of step", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/json" + } + }, + "tags": [ + "steps" + ], + "operationId": "steps-get", + "summary": "Get step", + "x-sdk-interface": "steps.get", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.getStepByNameOrId" + } + }, + "put": { + "x-entityId": { + "pathId": "params.name", + "pathName": "body.metadata.name" + }, + "x-action": "updateStep", + "parameters": [ + { + "description": "Name of step", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "metadata": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "spec": { + "type": "object", + "properties": {} + } + } + } + } + } + } + }, + "tags": [ + "steps" + ], + "operationId": "steps-replace", + "summary": "Replace", + "x-sdk-interface": "steps.replace", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.replaceStepByName" + } + }, + "delete": { + "x-entityId": { + "pathId": "params.name" + }, + "x-action": "stepDeleted", + "parameters": [ + { + "description": "Name of step", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/json" + } + }, + "tags": [ + "steps" + ], + "operationId": "steps-delete", + "summary": "Delete", + "x-sdk-interface": "steps.delete", + "x-endpoint": { + "isEndpoint": false, + "preMiddleware": [ + "auth.isAuthenticated" + ], + "postMiddleware": [ + "global.iseMiddleware" + ], + "handler": "steps.deleteStepByName" + } + } + }, + "/projects": { "post": { "x-action": "createProject", diff --git a/package.json b/package.json index 3d0e5e8ee..d8d3ea171 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codefresh", - "version": "0.20.3", + "version": "0.21.0", "description": "Codefresh command line utility", "main": "index.js", "preferGlobal": true,