diff --git a/src/commands/service/create.decorator.json b/src/commands/service/create.decorator.json new file mode 100644 index 000000000..69d430779 --- /dev/null +++ b/src/commands/service/create.decorator.json @@ -0,0 +1,82 @@ +{ + "command": "create ", + "alias": "c", + "description": "Add a new service into this initialized spk project repository", + "options": [ + { + "arg": "-c, --helm-chart-chart ", + "description": "bedrock helm chart name. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", + "defaultValue": "" + }, + { + "arg": "-r, --helm-chart-repository ", + "description": "bedrock helm chart repository. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", + "defaultValue": "" + }, + { + "arg": "-b, --helm-config-branch ", + "description": "bedrock custom helm chart configuration branch. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", + "defaultValue": "" + }, + { + "arg": "-p, --helm-config-path ", + "description": "bedrock custom helm chart configuration path. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", + "defaultValue": "" + }, + { + "arg": "-g, --helm-config-git ", + "description": "bedrock helm chart configuration git repository. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", + "defaultValue": "" + }, + { + "arg": "-d, --packages-dir ", + "description": "The directory containing the mono-repo packages.", + "defaultValue": "" + }, + { + "arg": "-n, --display-name ", + "description": "Display name of the service.", + "defaultValue": "" + }, + { + "arg": "-m, --maintainer-name ", + "description": "The name of the primary maintainer for this service.", + "defaultValue": "maintainer name" + }, + { + "arg": "-e, --maintainer-email ", + "description": "The email of the primary maintainer for this service.", + "defaultValue": "maintainer email" + }, + { + "arg": "--git-push", + "description": "SPK CLI will try to commit and push these changes to a new origin/branch named after the service.", + "defaultValue": false + }, + { + "arg": "--middlewares ", + "description": "Traefik2 middlewares you wish to to be injected into your Traefik2 IngressRoutes", + "defaultValue": "" + }, + { + "arg": "--k8s-backend-port ", + "description": "Kubernetes service port which this service is exposed with; will be used to configure Traefik2 IngressRoutes", + "defaultValue": "80" + }, + { + "arg": "--k8s-backend ", + "description": "Kubernetes backend service name; will be used to configure Traefik2 IngressRoutes", + "defaultValue": "" + }, + { + "arg": "--path-prefix ", + "description": "The path prefix for ingress route; will be used to configure Traefik2 IngressRoutes. If omitted, then the service name will used.", + "defaultValue": "" + }, + { + "arg": "--path-prefix-major-version ", + "description": "Version to be used in the path prefix; will be used to configure Traefik2 IngressRoutes. ie. 'v1' will result in a path prefix of '/v1/servicename", + "defaultValue": "" + } + ] +} diff --git a/src/commands/service/create.test.ts b/src/commands/service/create.test.ts index aab234823..e76a69c74 100644 --- a/src/commands/service/create.test.ts +++ b/src/commands/service/create.test.ts @@ -1,10 +1,12 @@ import fs from "fs"; -import os from "os"; import path from "path"; import { promisify } from "util"; import uuid from "uuid/v4"; import { Bedrock } from "../../config"; +import * as config from "../../config"; +import { DEFAULT_CONTENT as BedrockMockedContent } from "../../lib/bedrockYaml"; import { checkoutCommitPushCreatePRLink } from "../../lib/gitutils"; +import { createTempDir, removeDir } from "../../lib/ioUtil"; import { disableVerboseLogging, enableVerboseLogging, @@ -14,7 +16,7 @@ import { createTestBedrockYaml, createTestMaintainersYaml } from "../../test/mockFactory"; -import { createService, isValidConfig } from "./create"; +import { createService, execute, fetchValues, ICommandValues } from "./create"; jest.mock("../../lib/gitutils"); beforeAll(() => { @@ -25,38 +27,137 @@ afterAll(() => { disableVerboseLogging(); }); -describe("validate pipeline config", () => { - const configValues: any[] = [ - "testHelmChart", - "testHelmRepo", - "testHelmConfigBranch", - "testHelmConfigGit", - "/test/path", - "testService", - "test/packages", - "test-maintainer", - "test@maintainer.com", - "my,middleware,string", - true, - "testDisplayName", - 80, - "pathPrefix", - "version", - "backend" - ]; - - it("config is valid", () => { - expect(isValidConfig.apply(undefined, configValues as any)).toBe(true); +beforeEach(() => { + jest.clearAllMocks(); +}); + +const mockValues: ICommandValues = { + displayName: "", + gitPush: false, + helmChartChart: "", + helmChartRepository: "", + helmConfigBranch: "", + helmConfigGit: "", + helmConfigPath: "", + k8sBackend: "", + k8sBackendPort: "80", + k8sPort: 80, + maintainerEmail: "", + maintainerName: "", + middlewares: "", + middlewaresArray: [], + packagesDir: "", + pathPrefix: "", + pathPrefixMajorVersion: "", + variableGroups: [] +}; + +const getMockValues = (): ICommandValues => { + // TOFIX: if possible, can we use lodash? + return JSON.parse(JSON.stringify(mockValues)); +}; + +const validateDirNFiles = ( + dir: string, + serviceName: string, + values: ICommandValues +) => { + // Check temp test directory exists + expect(fs.existsSync(dir)).toBe(true); + + // Check service directory exists + const serviceDirPath = path.join(dir, values.packagesDir, serviceName); + expect(fs.existsSync(serviceDirPath)).toBe(true); + + // Verify new azure-pipelines created + const filepaths = ["azure-pipelines.yaml", "Dockerfile"].map(filename => + path.join(serviceDirPath, filename) + ); + + for (const filepath of filepaths) { + expect(fs.existsSync(filepath)).toBe(true); + } +}; + +describe("Test fetchValues function", () => { + it("Negative test: invalid port", () => { + const values = getMockValues(); + values.k8sBackendPort = "abc"; + try { + fetchValues(values); + expect(true).toBe(false); + } catch (err) { + expect(err).toBeDefined(); + } + }); + it("Postive test: with middlewares value", () => { + jest.spyOn(config, "Bedrock").mockReturnValueOnce(BedrockMockedContent); + const mocked = getMockValues(); + mocked.middlewares = "mid1, mid2"; // space after comma is intentional, expecting trimming to happen + const result = fetchValues(mocked); + expect(result.middlewaresArray).toEqual(["mid1", "mid2"]); }); + it("Postive test", () => { + const mocked = getMockValues(); + jest.spyOn(config, "Bedrock").mockReturnValueOnce(BedrockMockedContent); + const result = fetchValues(mocked); + expect(result).toEqual(mocked); + }); +}); - it("undefined parameters", () => { - for (const i of configValues.keys()) { - const configValuesWithInvalidValue = configValues.map((value, j) => - i === j ? undefined : value - ); - expect( - isValidConfig.apply(undefined, configValuesWithInvalidValue as any) - ).toBe(false); +describe("Test execute function", () => { + it("Negative test: without service name", async () => { + const exitFn = jest.fn(); + await execute("", getMockValues(), exitFn); + expect(exitFn).toBeCalledTimes(1); + expect(exitFn.mock.calls).toEqual([[1]]); + }); + it("Negative test: missing bedrock file", async () => { + const testServiceName = uuid(); + const exitFn = jest.fn(); + jest.spyOn(config, "bedrockFileInfo").mockImplementation(() => + Promise.resolve({ + exist: false, + hasVariableGroups: false + }) + ); + try { + await execute(testServiceName, getMockValues(), exitFn); + expect(exitFn).toBeCalledTimes(1); + expect(exitFn.mock.calls).toEqual([[1]]); + } finally { + removeDir(testServiceName); // housekeeping + } + }); + it("Negative test: missing bedrock variable groups", async () => { + const testServiceName = uuid(); + const exitFn = jest.fn(); + jest.spyOn(config, "bedrockFileInfo").mockImplementation(() => + Promise.resolve({ + exist: true, + hasVariableGroups: false + }) + ); + try { + await execute(testServiceName, getMockValues(), exitFn); + expect(exitFn).toBeCalledTimes(1); + expect(exitFn.mock.calls).toEqual([[1]]); + } finally { + removeDir(testServiceName); // housekeeping + } + }); + it("Negative test: simulated exception thrown", async () => { + const testServiceName = uuid(); + const exitFn = jest.fn(); + jest + .spyOn(config, "bedrockFileInfo") + .mockImplementation(() => Promise.reject()); + try { + await execute(testServiceName, getMockValues(), exitFn); + expect(exitFn).toBeCalledTimes(1); + expect(exitFn.mock.calls).toEqual([[1]]); + } finally { + removeDir(testServiceName); // housekeeping } }); }); @@ -65,8 +166,7 @@ describe("Adding a service to a repo directory", () => { let randomTmpDir: string = ""; beforeEach(async () => { // Create random directory to initialize - randomTmpDir = path.join(os.tmpdir(), uuid()); - fs.mkdirSync(randomTmpDir); + randomTmpDir = createTempDir(); }); test("New directory is created under root directory with required service files.", async () => { @@ -75,45 +175,23 @@ describe("Adding a service to a repo directory", () => { ); await writeSampleBedrockFileToDir(path.join(randomTmpDir, "bedrock.yaml")); - const packageDir = ""; - + const values = getMockValues(); + values.packagesDir = ""; + values.k8sPort = 1337; const serviceName = uuid(); logger.info( `creating randomTmpDir ${randomTmpDir} and service ${serviceName}` ); - // addService call - const k8sBackendPort = 1337; - await createService( - randomTmpDir, - serviceName, - packageDir, - false, - k8sBackendPort - ); - - // Check temp test directory exists - expect(fs.existsSync(randomTmpDir)).toBe(true); - - // Check service directory exists - const serviceDirPath = path.join(randomTmpDir, packageDir, serviceName); - expect(fs.existsSync(serviceDirPath)).toBe(true); - - // Verify new azure-pipelines created - const filepaths = ["azure-pipelines.yaml", "Dockerfile"].map(filename => - path.join(serviceDirPath, filename) - ); - - for (const filepath of filepaths) { - expect(fs.existsSync(filepath)).toBe(true); - } + await createService(randomTmpDir, serviceName, values); + validateDirNFiles(randomTmpDir, serviceName, values); // TODO: Verify root project bedrock.yaml and maintainers.yaml has been changed too. const bedrock = Bedrock(randomTmpDir); const newService = bedrock.services["./" + serviceName]; expect(newService).toBeDefined(); - expect(newService.k8sBackendPort).toBe(k8sBackendPort); + expect(newService.k8sBackendPort).toBe(values.k8sPort); }); test("New directory is created under '/packages' directory with required service files.", async () => { @@ -122,32 +200,18 @@ describe("Adding a service to a repo directory", () => { ); await writeSampleBedrockFileToDir(path.join(randomTmpDir, "bedrock.yaml")); - const packageDir = "packages"; + const values = getMockValues(); + values.packagesDir = "packages"; + values.k8sPort = 1337; const serviceName = uuid(); - const variableGroupName = uuid(); logger.info( `creating randomTmpDir ${randomTmpDir} and service ${serviceName}` ); // addService call - await createService(randomTmpDir, serviceName, "packages", false, 1337); - - // Check temp test directory exists - expect(fs.existsSync(randomTmpDir)).toBe(true); - - // Check service directory exists - const serviceDirPath = path.join(randomTmpDir, packageDir, serviceName); - expect(fs.existsSync(serviceDirPath)).toBe(true); - - // Verify new azure-pipelines and Dockerfile created - const filepaths = ["azure-pipelines.yaml", "Dockerfile"].map(filename => - path.join(serviceDirPath, filename) - ); - - for (const filepath of filepaths) { - expect(fs.existsSync(filepath)).toBe(true); - } + await createService(randomTmpDir, serviceName, values); + validateDirNFiles(randomTmpDir, serviceName, values); // TODO: Verify root project bedrock.yaml and maintainers.yaml has been changed too. }); @@ -158,32 +222,18 @@ describe("Adding a service to a repo directory", () => { ); await writeSampleBedrockFileToDir(path.join(randomTmpDir, "bedrock.yaml")); - const packageDir = "packages"; + const values = getMockValues(); + values.packagesDir = "packages"; + values.gitPush = true; + values.k8sPort = 1337; const serviceName = uuid(); - const variableGroupName = uuid(); logger.info( `creating randomTmpDir ${randomTmpDir} and service ${serviceName}` ); - // addService call - await createService(randomTmpDir, serviceName, "packages", true, 1337); - - // Check temp test directory exists - expect(fs.existsSync(randomTmpDir)).toBe(true); - - // Check service directory exists - const serviceDirPath = path.join(randomTmpDir, packageDir, serviceName); - expect(fs.existsSync(serviceDirPath)).toBe(true); - - // Verify new azure-pipelines and Dockerfile created - const filepaths = ["azure-pipelines.yaml", "Dockerfile"].map(filename => - path.join(serviceDirPath, filename) - ); - - for (const filepath of filepaths) { - expect(fs.existsSync(filepath)).toBe(true); - } + await createService(randomTmpDir, serviceName, values); + validateDirNFiles(randomTmpDir, serviceName, values); expect(checkoutCommitPushCreatePRLink).toHaveBeenCalled(); }); @@ -194,23 +244,17 @@ describe("Adding a service to a repo directory", () => { ); await writeSampleBedrockFileToDir(path.join(randomTmpDir, "bedrock.yaml")); - const packageDir = ""; + const values = getMockValues(); + values.k8sPort = 1337; const serviceName = uuid(); logger.info( `creating randomTmpDir ${randomTmpDir} and service ${serviceName}` ); // create service with no middleware - await createService(randomTmpDir, serviceName, packageDir, false, 1337); - - // Check temp test directory exists - expect(fs.existsSync(randomTmpDir)).toBe(true); - - // Check service directory exists - const serviceDirPath = path.join(randomTmpDir, packageDir, serviceName); - expect(fs.existsSync(serviceDirPath)).toBe(true); + await createService(randomTmpDir, serviceName, values); + validateDirNFiles(randomTmpDir, serviceName, values); - // get bedrock config const bedrockConfig = Bedrock(randomTmpDir); // check the added service has an empty list for middlewares @@ -231,26 +275,18 @@ describe("Adding a service to a repo directory", () => { ); await writeSampleBedrockFileToDir(path.join(randomTmpDir, "bedrock.yaml")); - const packageDir = ""; + const values = getMockValues(); + values.k8sPort = 1337; + values.middlewaresArray = ["foo", "bar", "baz"]; + const serviceName = uuid(); logger.info( `creating randomTmpDir ${randomTmpDir} and service ${serviceName}` ); - // add some middlewares - const middlewares = ["foo", "bar", "baz"]; - await createService(randomTmpDir, serviceName, packageDir, false, 1337, { - middlewares - }); - - // Check temp test directory exists - expect(fs.existsSync(randomTmpDir)).toBe(true); - - // Check service directory exists - const serviceDirPath = path.join(randomTmpDir, packageDir, serviceName); - expect(fs.existsSync(serviceDirPath)).toBe(true); + await createService(randomTmpDir, serviceName, values); + validateDirNFiles(randomTmpDir, serviceName, values); - // get bedrock config const bedrockConfig = Bedrock(randomTmpDir); // check that the added service has the expected middlewares @@ -260,8 +296,10 @@ describe("Adding a service to a repo directory", () => { if (servicePath.includes(serviceName)) { expect(service.middlewares).toBeDefined(); expect(Array.isArray(service.middlewares)).toBe(true); - expect(service.middlewares?.length).toBe(middlewares.length); - expect(service.middlewares).toStrictEqual(middlewares); + expect(service.middlewares?.length).toBe( + values.middlewaresArray.length + ); + expect(service.middlewares).toStrictEqual(values.middlewaresArray); } } }); diff --git a/src/commands/service/create.ts b/src/commands/service/create.ts index e0a7bbc34..c7cf9f7ae 100644 --- a/src/commands/service/create.ts +++ b/src/commands/service/create.ts @@ -10,6 +10,7 @@ import { addNewService as addNewServiceToBedrockFile, YAML_NAME as BedrockFileName } from "../../lib/bedrockYaml"; +import { build as buildCmd, exit as exitCmd } from "../../lib/commandBuilder"; import { addNewServiceToMaintainersFile, generateDockerfile, @@ -17,321 +18,129 @@ import { generateStarterAzurePipelinesYaml } from "../../lib/fileutils"; import { checkoutCommitPushCreatePRLink } from "../../lib/gitutils"; +import { isPortNumberString } from "../../lib/validator"; import { logger } from "../../logger"; import { IBedrockFileInfo, IHelmConfig, IUser } from "../../types"; +import decorator from "./create.decorator.json"; + +export interface ICommandOptions { + displayName: string; + gitPush: boolean; + helmChartChart: string; + helmChartRepository: string; + helmConfigBranch: string; + helmConfigGit: string; + helmConfigPath: string; + k8sBackend: string; + maintainerEmail: string; + maintainerName: string; + middlewares: string; + packagesDir: string; + pathPrefix: string; + pathPrefixMajorVersion: string; + k8sBackendPort: string; +} + +export interface ICommandValues extends ICommandOptions { + k8sPort: number; + middlewaresArray: string[]; + variableGroups: string[]; +} + +export const fetchValues = (opts: ICommandOptions) => { + if (!isPortNumberString(opts.k8sBackendPort)) { + throw new Error("value for --k8s-service-port is not a valid port number"); + } + + let variableGroups: string[] = []; + + const bedrock = Bedrock(); + variableGroups = bedrock.variableGroups || []; + + let middlewaresArray: string[] = []; + if (opts.middlewares && opts.middlewares.trim()) { + middlewaresArray = opts.middlewares.split(",").map(str => str.trim()); + } + + const values: ICommandValues = { + displayName: opts.displayName, + gitPush: opts.gitPush, + helmChartChart: opts.helmChartChart, + helmChartRepository: opts.helmChartRepository, + helmConfigBranch: opts.helmConfigBranch, + helmConfigGit: opts.helmConfigGit, + helmConfigPath: opts.helmConfigPath, + k8sBackend: opts.k8sBackend, + k8sBackendPort: opts.k8sBackendPort, + k8sPort: parseInt(opts.k8sBackendPort, 10), + maintainerEmail: opts.maintainerEmail, + maintainerName: opts.maintainerName, + middlewares: opts.middlewares, + middlewaresArray, + packagesDir: opts.packagesDir, + pathPrefix: opts.pathPrefix, + pathPrefixMajorVersion: opts.pathPrefixMajorVersion, + variableGroups + }; -/** - * Adds the create command to the service command object - * - * @param command Commander command object to decorate - */ -export const createCommandDecorator = (command: commander.Command): void => { - command - .command("create ") - .alias("c") - .description( - "Add a new service into this initialized spk project repository" - ) - .option( - "-c, --helm-chart-chart ", - "bedrock helm chart name. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", - "" - ) - .option( - "-r, --helm-chart-repository ", - "bedrock helm chart repository. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", - "" - ) - .option( - "-b, --helm-config-branch ", - "bedrock custom helm chart configuration branch. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", - "" - ) - .option( - "-p, --helm-config-path ", - "bedrock custom helm chart configuration path. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", - "" - ) - .option( - "-g, --helm-config-git ", - "bedrock helm chart configuration git repository. --helm-chart-* and --helm-config-* are exclusive; you may only use one.", - "" - ) - .option( - "-d, --packages-dir ", - "The directory containing the mono-repo packages.", - "" - ) - .option( - "-n, --display-name ", - "Display name of the service.", - "" - ) - .option( - "-m, --maintainer-name ", - "The name of the primary maintainer for this service.", - "maintainer name" - ) - .option( - "-e, --maintainer-email ", - "The email of the primary maintainer for this service.", - "maintainer email" - ) - .option( - "--git-push", - "SPK CLI will try to commit and push these changes to a new origin/branch named after the service.", - false - ) - .option( - "--middlewares ", - "Traefik2 middlewares you wish to to be injected into your Traefik2 IngressRoutes", - "" - ) - .option( - "--k8s-backend-port ", - "Kubernetes backend service port which this service is exposed with; will be used to configure Traefik2 IngressRoutes", - "80" - ) - .option( - "--k8s-backend ", - "Kubernetes backend service name; will be used to configure Traefik2 IngressRoutes", - "" - ) - .option( - "--path-prefix ", - "The path prefix for ingress route; will be used to configure Traefik2 IngressRoutes. If omitted, then the service name will used.", - "" - ) - .option( - "--path-prefix-major-version ", - "Version to be used in the path prefix; will be used to configure Traefik2 IngressRoutes. ie. 'v1' will result in a path prefix of '/v1/servicename", - "" - ) - .action(async (serviceName, opts) => { - const projectPath = process.cwd(); - logger.verbose(`project path: ${projectPath}`); - - const fileInfo: IBedrockFileInfo = await bedrockFileInfo(projectPath); - if (fileInfo.exist === false) { - logger.error(projectInitCvgDependencyErrorMessage); - return undefined; - } else if (fileInfo.hasVariableGroups === false) { - logger.error(projectCvgDependencyErrorMessage); - return undefined; - } - - const bedrock = Bedrock(); - - const { - displayName, - gitPush, - helmChartChart, - helmChartRepository, - helmConfigBranch, - helmConfigGit, - helmConfigPath, - k8sBackend, - maintainerEmail, - maintainerName, - middlewares, - packagesDir, - pathPrefix, - pathPrefixMajorVersion - } = opts; - const k8sPort = Number(opts.k8sBackendPort); - const variableGroups = bedrock?.variableGroups; - - try { - if ( - !isValidConfig( - helmChartChart, - helmChartRepository, - helmConfigBranch, - helmConfigGit, - helmConfigPath, - serviceName, - packagesDir, - maintainerName, - maintainerEmail, - middlewares, - gitPush, - displayName, - k8sPort, - pathPrefix, - pathPrefixMajorVersion, - k8sBackend - ) - ) { - throw Error(`Invalid configuration provided`); - } + // Values do not need to be validated + // as they are mostly provided by Commander. + return values; +}; - await createService( - projectPath, - serviceName, - packagesDir, - gitPush, - k8sPort, - { - displayName, - helmChartChart, - helmChartRepository, - helmConfigBranch, - helmConfigGit, - helmConfigPath, - k8sBackend, - maintainerEmail, - maintainerName, - middlewares: (middlewares as string) - .split(",") - .map(str => str.trim()), - pathPrefix, - pathPrefixMajorVersion, - variableGroups - } - ); - } catch (err) { - logger.error( - `Error occurred adding service ${serviceName} to project ${projectPath}` - ); - logger.error(err); - process.exit(1); - } - }); +export const execute = async ( + serviceName: string, + opts: ICommandOptions, + exitFn: (status: number) => Promise +) => { + if (!serviceName) { + logger.error("Service name is missing"); + await exitFn(1); + return; + } + + const projectPath = process.cwd(); + logger.verbose(`project path: ${projectPath}`); + + try { + const fileInfo: IBedrockFileInfo = await bedrockFileInfo(projectPath); + if (fileInfo.exist === false) { + logger.error(projectInitCvgDependencyErrorMessage()); + await exitFn(1); + return; + } + + if (fileInfo.hasVariableGroups === false) { + logger.error(projectCvgDependencyErrorMessage()); + await exitFn(1); + return; + } + + const values = fetchValues(opts); + await createService(projectPath, serviceName, values); + await exitFn(0); + } catch (err) { + logger.error( + `Error occurred adding service ${serviceName} to project ${projectPath}` + ); + logger.error(err); + await exitFn(1); + } }; /** - * Validates the pipeline configuration + * Adds the create command to the service command object * - * @param helmChartChart - * @param helmChartRepository - * @param helmConfigBranch - * @param helmConfigGit - * @param helmConfigPath - * @param serviceName - * @param packagesDir - * @param maintainerName - * @param maintainerEmail - * @param middlewares - * @param gitPush - * @param displayName - * @param k8sPort - * @param pathPrefix - * @param pathPrefixMajorVersion - * @param k8sBackend + * @param command Commander command object to decorate */ -export const isValidConfig = ( - helmChartChart: any, - helmChartRepository: any, - helmConfigBranch: any, - helmConfigGit: any, - helmConfigPath: any, - serviceName: any, - packagesDir: any, - maintainerName: any, - maintainerEmail: any, - middlewares: any, - gitPush: any, - displayName: any, - k8sPort: any, - pathPrefix?: any, - pathPrefixMajorVersion?: any, - k8sBackend?: any -): boolean => { - const missingConfig = []; - - // Type check all parsed command line args here. - if (typeof helmChartChart !== "string") { - missingConfig.push( - `helmChartChart must be of type 'string', ${typeof helmChartChart} given.` - ); - } - if (typeof helmChartRepository !== "string") { - missingConfig.push( - `helmChartRepository must be of type 'string', ${typeof helmChartRepository} given.` - ); - } - if (typeof helmConfigBranch !== "string") { - missingConfig.push( - `helmConfigBranch must be of type 'string', ${typeof helmConfigBranch} given.` - ); - } - if (typeof helmConfigGit !== "string") { - missingConfig.push( - `helmConfigGit must be of type 'string', ${typeof helmConfigGit} given.` - ); - } - if (typeof helmConfigPath !== "string") { - missingConfig.push( - `helmConfigPath must be of type 'string', ${typeof helmConfigPath} given.` - ); - } - if (typeof serviceName !== "string") { - missingConfig.push( - `serviceName must be of type 'string', ${typeof serviceName} given.` - ); - } - if (typeof displayName !== "string") { - missingConfig.push( - `displayName must be of type 'string', ${typeof displayName} given.` - ); - } - if (typeof packagesDir !== "string") { - missingConfig.push( - `packagesDir must be of type 'string', ${typeof packagesDir} given.` - ); - } - if (typeof maintainerName !== "string") { - missingConfig.push( - `maintainerName must be of type 'string', ${typeof maintainerName} given.` - ); - } - if (typeof maintainerEmail !== "string") { - missingConfig.push( - `maintainerEmail must be of type 'string', ${typeof maintainerEmail} given.` - ); - } - if (typeof middlewares !== "string") { - missingConfig.push( - `middlewares must be of type of 'string', ${typeof middlewares} given.` - ); - } - if (typeof gitPush !== "boolean") { - missingConfig.push( - `gitPush must be of type 'boolean', ${typeof gitPush} given.` - ); - } - // k8sPort has to be a positive integer - if ( - typeof k8sPort !== "number" || - !Number.isInteger(k8sPort) || - k8sPort < 0 - ) { - missingConfig.push( - `k8s-port must be a positive integer, parsed ${k8sPort} from input.` - ); - } - if (typeof k8sBackend !== "string") { - missingConfig.push( - `k8s-backend must be of type 'string', ${typeof k8sBackend} given.` - ); - } - if (typeof pathPrefix !== "string") { - missingConfig.push( - `pathPrefix must be of type 'string', ${typeof pathPrefix} given.` - ); - } - if (typeof pathPrefixMajorVersion !== "string") { - missingConfig.push( - `path-prefix-major-version must be of type 'string', ${typeof pathPrefixMajorVersion} given.` - ); - } - - if (missingConfig.length > 0) { - logger.error("Error in configuration: " + missingConfig.join(" ")); - return false; - } - - return true; +export const createCommandDecorator = (command: commander.Command): void => { + buildCmd(command, decorator).action( + async (serviceName: string, opts: ICommandOptions) => { + await execute(serviceName, opts, async (status: number) => { + await exitCmd(logger, process.exit, status); + }); + } + ); }; /** @@ -347,57 +156,27 @@ export const isValidConfig = ( export const createService = async ( rootProjectPath: string, serviceName: string, - packagesDir: string, - gitPush: boolean, - k8sBackendPort: number, - opts?: { - displayName?: string; - helmChartChart?: string; - helmChartRepository?: string; - helmConfigBranch?: string; - helmConfigGit?: string; - helmConfigPath?: string; - k8sBackend?: string; - maintainerEmail?: string; - maintainerName?: string; - middlewares?: string[]; - pathPrefix?: string; - pathPrefixMajorVersion?: string; - variableGroups?: string[]; - } + values: ICommandValues ) => { - const { - displayName = "", - helmChartChart = "", - helmChartRepository = "", - helmConfigBranch = "", - helmConfigPath = "", - helmConfigGit = "", - k8sBackend = "", - maintainerName = "", - maintainerEmail = "", - middlewares = [], - variableGroups = [], - pathPrefix = "", - pathPrefixMajorVersion = "" - } = opts ?? {}; - logger.info( - `Adding Service: ${serviceName}, to Project: ${rootProjectPath} under directory: ${packagesDir}` + `Adding Service: ${serviceName}, to Project: ${rootProjectPath} under directory: ${values.packagesDir}` ); logger.info( - `DisplayName: ${displayName}, MaintainerName: ${maintainerName}, MaintainerEmail: ${maintainerEmail}` + `DisplayName: ${values.displayName}, MaintainerName: ${values.maintainerName}, MaintainerEmail: ${values.maintainerEmail}` ); - const newServiceDir = path.join(rootProjectPath, packagesDir, serviceName); + const newServiceDir = path.join( + rootProjectPath, + values.packagesDir, + serviceName + ); logger.info(`servicePath: ${newServiceDir}`); - // Mkdir shelljs.mkdir("-p", newServiceDir); // Create azure pipelines yaml in directory await generateStarterAzurePipelinesYaml(rootProjectPath, newServiceDir, { - variableGroups + variableGroups: values.variableGroups }); // Create empty .gitignore file in directory @@ -408,8 +187,8 @@ export const createService = async ( // add maintainers to file in parent repo file const newUser = { - email: maintainerEmail, - name: maintainerName + email: values.maintainerEmail, + name: values.maintainerName } as IUser; const newServiceRelativeDir = path.relative(rootProjectPath, newServiceDir); @@ -423,38 +202,36 @@ export const createService = async ( // Add relevant bedrock info to parent bedrock.yaml - let helmConfig: IHelmConfig; - if (helmChartChart && helmChartRepository) { - helmConfig = { - chart: { - chart: helmChartChart, - repository: helmChartRepository - } - }; - } else { - helmConfig = { - chart: { - branch: helmConfigBranch, - git: helmConfigGit, - path: helmConfigPath - } - }; - } + const helmConfig: IHelmConfig = + values.helmChartChart && values.helmChartRepository + ? { + chart: { + chart: values.helmChartChart, + repository: values.helmChartRepository + } + } + : { + chart: { + branch: values.helmConfigBranch, + git: values.helmConfigGit, + path: values.helmConfigPath + } + }; addNewServiceToBedrockFile( rootProjectPath, newServiceRelativeDir, - displayName, + values.displayName, helmConfig, - middlewares, - k8sBackendPort, - k8sBackend, - pathPrefix, - pathPrefixMajorVersion + values.middlewaresArray, + values.k8sPort, + values.k8sBackend, + values.pathPrefix, + values.pathPrefixMajorVersion ); // If requested, create new git branch, commit, and push - if (gitPush) { + if (values.gitPush) { await checkoutCommitPushCreatePRLink( serviceName, newServiceDir, diff --git a/src/lib/ioUtil.test.ts b/src/lib/ioUtil.test.ts index 7358d75ed..2ee2648c3 100644 --- a/src/lib/ioUtil.test.ts +++ b/src/lib/ioUtil.test.ts @@ -1,5 +1,6 @@ import fs from "fs"; import path from "path"; +import uuid from "uuid/v4"; import { createTempDir, getMissingFilenames, @@ -26,6 +27,9 @@ describe("test createTempDir function", () => { }); describe("test removeDir", () => { + it("non exist directory", () => { + removeDir(uuid()); // no exception thrown + }); it("empty directory", () => { const name = createTempDir(); removeDir(name); diff --git a/src/lib/ioUtil.ts b/src/lib/ioUtil.ts index 08c28b2a5..539debf9b 100644 --- a/src/lib/ioUtil.ts +++ b/src/lib/ioUtil.ts @@ -24,15 +24,17 @@ export const createTempDir = (parent?: string): string => { */ export const removeDir = (dir: string) => { const folder = path.resolve(dir); - fs.readdirSync(folder).forEach(item => { - const curPath = path.join(folder, item); - if (fs.statSync(curPath).isDirectory()) { - removeDir(curPath); - } else { - fs.unlinkSync(curPath); - } - }); - fs.rmdirSync(folder); + if (fs.existsSync(folder)) { + fs.readdirSync(folder).forEach(item => { + const curPath = path.join(folder, item); + if (fs.statSync(curPath).isDirectory()) { + removeDir(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(folder); + } }; /** diff --git a/src/lib/validator.test.ts b/src/lib/validator.test.ts index afa2d3a7d..68bc45ae6 100644 --- a/src/lib/validator.test.ts +++ b/src/lib/validator.test.ts @@ -1,4 +1,9 @@ -import { hasValue, validateForNonEmptyValue } from "./validator"; +import { + hasValue, + isIntegerString, + isPortNumberString, + validateForNonEmptyValue +} from "./validator"; describe("Tests on validator helper functions", () => { it("Test hasValue function", () => { @@ -10,6 +15,31 @@ describe("Tests on validator helper functions", () => { expect(hasValue(" b ")).toBe(true); expect(hasValue(" a ")).toBe(true); }); + it("Test isIntegerString function", () => { + expect(isIntegerString("")).toBe(false); + expect(isIntegerString(undefined)).toBe(false); + expect(isIntegerString(null)).toBe(false); + + expect(isIntegerString("-10")).toBe(false); + expect(isIntegerString("+10")).toBe(false); + expect(isIntegerString("010")).toBe(false); + expect(isIntegerString("10.0")).toBe(false); + expect(isIntegerString("80")).toBe(true); + }); + it("Test isPortNumberString function", () => { + expect(isPortNumberString("")).toBe(false); + expect(isPortNumberString(undefined)).toBe(false); + expect(isPortNumberString(null)).toBe(false); + + expect(isPortNumberString("-10")).toBe(false); + expect(isPortNumberString("+10")).toBe(false); + expect(isPortNumberString("010")).toBe(false); + expect(isPortNumberString("10.0")).toBe(false); + expect(isPortNumberString("80")).toBe(true); + expect(isPortNumberString("8080")).toBe(true); + expect(isPortNumberString("0")).toBe(false); + expect(isPortNumberString("65536")).toBe(false); + }); it("Test validateForNonEmptyValue function", () => { // expect "error" to be returned ["", undefined, null].forEach(val => { diff --git a/src/lib/validator.ts b/src/lib/validator.ts index d91df3a8b..5cfe8c072 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -15,6 +15,35 @@ export const hasValue = (val: undefined | null | string): val is string => { return val !== undefined && val !== null && val !== ""; }; +/** + * Returns true of val is not undefined, not null and not an empty string + * and can be converted to integer. + * + * @param val Value to inspect + */ +export const isIntegerString = (val: unknown): val is string => { + if (typeof val !== "string") { + return false; + } + if (val === undefined || val === null || val === "") { + return false; + } + return /^[1-9]\d+$/.test(val); +}; + +/** + * Returns true of val is a port number. + * + * @param val Value to inspect + */ +export const isPortNumberString = (val: unknown): val is string => { + if (!isIntegerString(val)) { + return false; + } + const port = parseInt(val, 10); + return port > 0 && port <= 65535; +}; + /** * Returns err if val is undefined, null or empty string. Returns * otherwise empty string.