import { fail } from "assert" ; import commander from "commander" ; import path from "path" ; import shelljs from "shelljs" ; import * as sshUrl from "ssh-url" ; import { Bedrock } from "../../config" ; import { addNewService as addNewServiceToBedrockFile , fileInfo as bedrockFileInfo , YAML_NAME as BedrockFileName , } from "../../lib/bedrockYaml" ; import { build as buildCmd , exit as exitCmd } from "../../lib/commandBuilder" ; import { addNewServiceToMaintainersFile , generateDockerfile , generateGitIgnoreFile , generateServiceBuildAndUpdatePipelineYaml , } from "../../lib/fileutils" ; import { checkoutCommitPushCreatePRLink } from "../../lib/gitpr" ; import * as dns from "../../lib/net/dns" ; import { isPortNumberString } from "../../lib/validator" ; import { logger } from "../../logger" ; import { BedrockFileInfo , HelmConfig , User } from "../../types" ; import decorator from "./create.decorator.json" ; import { build as buildError , log as logError } from "../../lib/errorBuilder" ; import { errorStatusCode } from "../../lib/errorStatusCode" ; export interface CommandOptions { gitPush : boolean ; helmChartChart : string ; helmChartRepository : string ; helmConfigAccessTokenVariable : string ; helmConfigBranch : string ; helmConfigGit : string ; helmConfigPath : string ; k8sBackend : string ; k8sBackendPort : string ; maintainerEmail : string ; maintainerName : string ; middlewares : string ; packagesDir : string ; pathPrefix : string ; pathPrefixMajorVersion : string ; serviceBuildVg : string ; serviceBuildVariables : string ; } export interface CommandValues extends CommandOptions { k8sPort : number ; middlewaresArray : string [ ] ; ringNames : string [ ] ; variableGroups : string [ ] ; serviceVgArray : string [ ] ; serviceVariablesArray : string [ ] ; } export const validUpperUnderscoreCase = ( segment ) : boolean => { return ! ! segment . match ( /^[A-Z0-9_]+$/ ) ; } ; export const fetchValues = ( opts ) : CommandValues => { if ( ! isPortNumberString ( opts . k8sBackendPort ) ) { throw buildError ( errorStatusCode . VALIDATION_ERR , "service-create-cmd-port-invalid-err" ) ; } const bedrock = Bedrock ( ) ; const variableGroups = bedrock . variableGroups ?? [ ] ; const rings = Object . keys ( bedrock . rings ) ; let middlewaresArray = [ ] ; if ( opts . middlewares && opts . middlewares . trim ( ) ) { middlewaresArray = opts . middlewares . split ( "," ) . map ( ( str ) => str . trim ( ) ) ; } let serviceVgArray = [ ] ; if ( opts . serviceBuildVg && opts . serviceBuildVg . trim ( ) ) { serviceVgArray = opts . serviceBuildVg . split ( "," ) . map ( ( str ) => str . trim ( ) ) ; } let serviceVariablesArray = [ ] ; if ( opts . serviceBuildVariables && opts . serviceBuildVariables . trim ( ) ) { serviceVariablesArray = opts . serviceBuildVariables . split ( "," ) . map ( ( str ) => str . trim ( ) ) ; } for ( const r of serviceVariablesArray ) { if ( ! validUpperUnderscoreCase ( r ) ) { logger . warn ( `${ r } is not in a valid format. Valid strings include only uppercases, numbers, and underscores.` ) ; } } if ( serviceVariablesArray . includes ( "" ) || serviceVgArray . includes ( "" ) ) { throw buildError ( errorStatusCode . VALIDATION_ERR , "service-create-cmd-invalid-service-variable-group-err" ) ; } const values = { gitPush : opts . gitPush , helmChartChart : opts . helmChartChart , helmChartRepository : opts . helmChartRepository , helmConfigAccessTokenVariable : opts . helmConfigAccessTokenVariable , 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 , ringNames : rings , variableGroups , serviceBuildVg : opts . serviceBuildVg , serviceVgArray , serviceBuildVariables : opts . serviceBuildVariables , serviceVariablesArray , } ; return values ; } ; export const checkDependencies = ( projectPath ) : void => { const fileInfo = bedrockFileInfo ( projectPath ) ; if ( fileInfo . exist === false ) { throw buildError ( errorStatusCode . CMD_EXE_ERR , "service-create-cmd-init-dependency-err" ) ; } else if ( fileInfo . hasVariableGroups === false ) { throw buildError ( errorStatusCode . CMD_EXE_ERR , "service-create-cmd-init-cvg-dependency-err" ) ; } } ; throws export const assertValidDnsInputs = ( opts ) : void => { if ( opts . k8sBackend ) { dns . assertIsValid ( "--k8s-backend" , opts . k8sBackend ) ; } if ( opts . pathPrefix ) { dns . assertIsValid ( "--path-prefix" , opts . pathPrefix ) ; } if ( opts . pathPrefixMajorVersion ) { dns . assertIsValid ( "--path-prefix-major-version" , opts . pathPrefixMajorVersion ) ; } } ; param rootProjectPath param serviceName param values export const createService = async ( rootProjectPath , serviceName , servicePath , values ) : Promise < void > => { logger . info ( `Adding Service: ${ serviceName }, located at ${ servicePath }, to Project: ${ rootProjectPath } under directory: ${ values . packagesDir }` ) ; const newServiceDir = path . join ( rootProjectPath , values . packagesDir , servicePath ) ; logger . info ( `servicePath: ${ newServiceDir }` ) ; shelljs . mkdir ( "-p" , newServiceDir ) ; generateServiceBuildAndUpdatePipelineYaml ( rootProjectPath , values . ringNames , serviceName , newServiceDir , values . variableGroups , values . serviceVgArray , values . serviceVariablesArray ) ; generateGitIgnoreFile ( newServiceDir , [ "" ] ) ; generateDockerfile ( newServiceDir ) ; const newUser = { email : values . maintainerEmail , name : values . maintainerName , } as User ; const newServiceRelativeDir = path . relative ( rootProjectPath , newServiceDir ) ; logger . debug ( `newServiceRelPath: ${ newServiceRelativeDir }` ) ; addNewServiceToMaintainersFile ( path . join ( rootProjectPath , "maintainers.yaml" ) , newServiceRelativeDir , [ newUser ] ) ; const helmConfig = values . helmChartChart && values . helmChartRepository ? { chart : { chart : values . helmChartChart , repository : values . helmChartRepository , } , } : { chart : { accessTokenVariable : values . helmConfigAccessTokenVariable , branch : values . helmConfigBranch , git : values . helmConfigGit , path : values . helmConfigPath , } , } ; addNewServiceToBedrockFile ( rootProjectPath , newServiceRelativeDir , serviceName , helmConfig , values . middlewaresArray , values . k8sPort , values . k8sBackend , values . pathPrefix , values . pathPrefixMajorVersion , values . serviceVgArray , values . serviceVariablesArray ) ; if ( values . gitPush ) { await checkoutCommitPushCreatePRLink ( serviceName , newServiceDir , path . join ( rootProjectPath , BedrockFileName ) , path . join ( rootProjectPath , "maintainers.yaml" ) ) ; } } ; param gitUrl param exitFn export const validateGitUrl = async ( gitUrl , exitFn ) : Promise < void > => { if ( gitUrl === "" ) { return ; } let isHelmConfigHttp = true ; try { new URL ( gitUrl ) ; } catch ( err ) { logger . warn ( `Provided helm git URL is an invalid http/https URL: ${ gitUrl }` ) ; isHelmConfigHttp = false ; } if ( ! isHelmConfigHttp ) { try { const parsedSshUrl = sshUrl . parse ( gitUrl ) ; if ( parsedSshUrl . user === null ) { fail ( "Not a valid git+ssh url" ) ; } } catch ( err ) { logError ( buildError ( errorStatusCode . VALIDATION_ERR , { errorKey : "service-create-cmd-invalid-helm-url-err" , values : [ gitUrl ] , } , err ) ) ; await exitFn ( 1 ) ; return ; } } } ; export const execute = async ( serviceName , servicePath , opts , exitFn ) : Promise < void > => { try { if ( ! serviceName ) { throw buildError ( errorStatusCode . CMD_EXE_ERR , "service-create-cmd-service-name-missing-err" ) ; } if ( ! servicePath ) { throw buildError ( errorStatusCode . CMD_EXE_ERR , "service-create-cmd-service-path-missing-err" ) ; } try { dns . assertIsValid ( "<service-name>" , serviceName ) ; assertValidDnsInputs ( opts ) ; } catch ( err ) { throw buildError ( errorStatusCode . CMD_EXE_ERR , "service-create-cmd-service-name-dns-invalid-err" , err ) ; } await validateGitUrl ( opts . helmConfigGit , exitFn ) ; const projectPath = process . cwd ( ) ; logger . verbose ( `project path: ${ projectPath }` ) ; checkDependencies ( projectPath ) ; const values = fetchValues ( opts ) ; await createService ( projectPath , serviceName , servicePath , values ) ; await exitFn ( 0 ) ; } catch ( err ) { logError ( buildError ( errorStatusCode . CMD_EXE_ERR , "service-create-cmd-failed" , err ) ) ; await exitFn ( 1 ) ; } } ; param command export const commandDecorator = ( command ) : void => { buildCmd ( command , decorator ) . action ( async ( serviceName , servicePath , opts ) => { await execute ( serviceName , servicePath , opts , async ( status ) => { await exitCmd ( logger , process . exit , status ) ; } ) ; } ) ; } ;