diff --git a/packages/core/src/scratchorg/pool/OrphanedOrgsDeleteImpl.ts b/packages/core/src/scratchorg/pool/OrphanedOrgsDeleteImpl.ts new file mode 100644 index 000000000..24f8078bd --- /dev/null +++ b/packages/core/src/scratchorg/pool/OrphanedOrgsDeleteImpl.ts @@ -0,0 +1,47 @@ +import SFPLogger, { Logger, LoggerLevel } from '@dxatscale/sfp-logger'; +import { Org } from '@salesforce/core'; +import { PoolBaseImpl } from './PoolBaseImpl'; +import ScratchOrg from '../ScratchOrg'; +import ScratchOrgInfoFetcher from './services/fetchers/ScratchOrgInfoFetcher'; +import ScratchOrgOperator from '../ScratchOrgOperator'; + +export default class OrphanedOrgsDeleteImpl extends PoolBaseImpl { + public constructor(hubOrg: Org, private logger:Logger) { + super(hubOrg); + this.hubOrg = hubOrg; + } + + protected async onExec(): Promise { + const results = (await new ScratchOrgInfoFetcher(this.hubOrg).getOrphanedScratchOrgs()) as any; + + let scratchOrgToDelete: ScratchOrg[] = new Array(); + if (results.records.length > 0) { + let scrathOrgIds: string[] = []; + for (let element of results.records) { + if (element.Description?.includes(`"requestedBy":"sfpowerscripts"`)) { + let soDetail: ScratchOrg = {}; + soDetail.orgId = element.ScratchOrg; + soDetail.username = element.SignupUsername; + soDetail.status = 'recovered'; + scratchOrgToDelete.push(soDetail); + scrathOrgIds.push(`'${element.Id}'`); + } + } + + if (scrathOrgIds.length > 0) { + let activeScrathOrgs = await new ScratchOrgInfoFetcher(this.hubOrg).getActiveScratchOrgsByInfoId( + scrathOrgIds.join(',') + ); + + if (activeScrathOrgs.records.length > 0) { + for (let scratchOrg of activeScrathOrgs.records) { + await new ScratchOrgOperator(this.hubOrg).delete(scratchOrg.Id); + SFPLogger.log(`Scratch org with username ${scratchOrg.SignupUsername} is recovered`,LoggerLevel.TRACE,this.logger); + } + } + } + } + + return scratchOrgToDelete; + } +} diff --git a/packages/core/src/scratchorg/pool/PoolCreateImpl.ts b/packages/core/src/scratchorg/pool/PoolCreateImpl.ts index efff5eded..f50df6b41 100644 --- a/packages/core/src/scratchorg/pool/PoolCreateImpl.ts +++ b/packages/core/src/scratchorg/pool/PoolCreateImpl.ts @@ -20,6 +20,8 @@ import PoolFetchImpl from './PoolFetchImpl'; import { COLOR_SUCCESS } from '@dxatscale/sfp-logger'; import { COLOR_ERROR } from '@dxatscale/sfp-logger'; import getFormattedTime from '../../utils/GetFormattedTime'; +import OrphanedOrgsDeleteImpl from './OrphanedOrgsDeleteImpl'; +import path from 'path'; export default class PoolCreateImpl extends PoolBaseImpl { private limiter; @@ -48,7 +50,8 @@ export default class PoolCreateImpl extends PoolBaseImpl { protected async onExec(): Promise> { await this.hubOrg.refreshAuth(); - let scriptExecPromises: Array> = []; + const scriptExecPromises: Array> = []; + //fetch current status limits this.limits = await new ScratchOrgLimitsFetcher(this.hubOrg).getScratchOrgLimits(); @@ -120,8 +123,8 @@ export default class PoolCreateImpl extends PoolBaseImpl { } // Assign workers to executed scripts - for (let scratchOrg of this.pool.scratchOrgs) { - let result = this.scriptExecutorWrappedForBottleneck(scratchOrg, this.hubOrg.getUsername()); + for (const scratchOrg of this.pool.scratchOrgs) { + const result = this.scriptExecutorWrappedForBottleneck(scratchOrg, this.hubOrg.getUsername()); scriptExecPromises.push(result); } @@ -142,11 +145,13 @@ export default class PoolCreateImpl extends PoolBaseImpl { }); } return ok(this.pool); + + } private async computeAllocation(): Promise { //Compute current pool requirement - let activeCount = await this.scratchOrgInfoFetcher.getCountOfActiveScratchOrgsByTag(this.pool.tag); + const activeCount = await this.scratchOrgInfoFetcher.getCountOfActiveScratchOrgsByTag(this.pool.tag); return this.allocateScratchOrgsPerTag(this.limits.ActiveScratchOrgs.Remaining, activeCount, this.pool); } @@ -183,15 +188,17 @@ export default class PoolCreateImpl extends PoolBaseImpl { //Generate Scratch Orgs SFPLogger.log(COLOR_KEY_MESSAGE('Generate Scratch Orgs..'), LoggerLevel.INFO); - let scratchOrgPromises = new Array>(); + const scratchOrgPromises = new Array>(); const scratchOrgCreationLimiter = new Bottleneck({ maxConcurrent: pool.batchSize, }); - let startTime = Date.now(); + addDescriptionToScratchOrg(pool); + + const startTime = Date.now(); for (let i = 1; i <= pool.to_allocate; i++) { - let scratchOrgPromise: Promise = scratchOrgCreationLimiter.schedule(() => + const scratchOrgPromise: Promise = scratchOrgCreationLimiter.schedule(() => scratchOrgOperator.create(`SO` + i, this.pool.configFilePath, this.pool.expiry, this.pool.waitTime) ); scratchOrgPromises.push(scratchOrgPromise); @@ -199,7 +206,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { SFPLogger.log(`Waiting for all scratch org request to complete, Please wait`); //Wait for all orgs to be created - let scratchOrgCreationResults = await Promise.allSettled(scratchOrgPromises); + const scratchOrgCreationResults = await Promise.allSettled(scratchOrgPromises); //Only worry about scrath orgs that have suceeded const isFulfilled = (p: PromiseSettledResult): p is PromiseFulfilledResult => p.status === 'fulfilled'; const isRejected = (p: PromiseSettledResult): p is PromiseRejectedResult => p.status === 'rejected'; @@ -209,7 +216,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { for (const reason of rejectedScratchOrgs) { if (reason.message.includes(`The client has timed out`)) { //Log how many we were able to create - let elapsedTime = Date.now() - startTime; + const elapsedTime = Date.now() - startTime; SFPLogger.log( `A scratch org creation was rejected due to saleforce not responding within the set wait time of ${pool.waitTime} mins \n` + `Time elasped so far ${COLOR_KEY_MESSAGE( @@ -220,7 +227,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { } //Log how many we were able to create - let elapsedTime = Date.now() - startTime; + const elapsedTime = Date.now() - startTime; SFPLogger.log( `Created ${COLOR_SUCCESS(scratchOrgs.length)} of ${pool.to_allocate} successfully with ${COLOR_ERROR( rejectedScratchOrgs.length @@ -233,7 +240,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { let index = scratchOrgs.length; while (index--) { try { - let orgDetails = await new OrgDetailsFetcher(scratchOrgs[index].username).getOrgDetails(); + const orgDetails = await new OrgDetailsFetcher(scratchOrgs[index].username).getOrgDetails(); if (orgDetails.status === 'Deleted') { throw new Error( `Throwing away scratch org ${this.pool.scratchOrgs[index].alias} as it has a status of deleted` @@ -246,7 +253,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { scratchOrgs = await this.scratchOrgInfoFetcher.getScratchOrgRecordId(scratchOrgs); - let scratchOrgInprogress = []; + const scratchOrgInprogress = []; scratchOrgs.forEach((scratchOrg) => { scratchOrgInprogress.push({ @@ -264,6 +271,43 @@ export default class PoolCreateImpl extends PoolBaseImpl { } return scratchOrgs; } else throw new Error(`No scratch orgs were sucesfully generated`); + + function addDescriptionToScratchOrg(pool: PoolConfig) { + + const configClonePath = path.join('.sfpowerscripts','scratchorg-configs',`${ makeFileId(8)}.json`); + fs.mkdirpSync('.sfpowerscripts/scratchorg-configs'); + fs.copyFileSync(pool.configFilePath,configClonePath); + + const scratchOrgDefn = fs.readJSONSync(configClonePath); + if (!scratchOrgDefn.description) + scratchOrgDefn.description = JSON.stringify({ + requestedBy: 'sfpowerscripts', + pool: pool.tag, + requestedAt: new Date().toISOString(), + }); + else + scratchOrgDefn.description = scratchOrgDefn.description.concat( + ' ', + JSON.stringify({ + requestedBy: 'sfpowerscripts', + pool: pool.tag, + requestedAt: new Date().toISOString(), + }) + ); + fs.writeJSONSync(configClonePath, scratchOrgDefn, { spaces: 4 }); + pool.configFilePath = configClonePath; + } + + function makeFileId(length): string { + let result = ''; + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + } } private async fetchScratchOrgsFromSnapshotPool( @@ -290,9 +334,9 @@ export default class PoolCreateImpl extends PoolBaseImpl { ).execute()) as ScratchOrg[]; scratchOrgs = await scratchOrgInfoFetcher.getScratchOrgRecordId(scratchOrgs); - let scratchOrgInprogress = []; + const scratchOrgInprogress = []; - if (scratchOrgs && scratchOrgs.length>0) { + if (scratchOrgs && scratchOrgs.length > 0) { scratchOrgs.forEach((scratchOrg) => { scratchOrgInprogress.push({ Id: scratchOrg.recordId, @@ -308,9 +352,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { await scratchOrgInfoAssigner.setScratchOrgInfo(scratchOrgInprogress); } return scratchOrgs; - } - else - { + } else { throw new Error('No scratch orgs were found to be fetched'); } } @@ -322,7 +364,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { ) { pool.failedToCreate = 0; for (let i = pool.scratchOrgs.length - 1; i >= 0; i--) { - let scratchOrg = pool.scratchOrgs[i]; + const scratchOrg = pool.scratchOrgs[i]; if (scratchOrg.isScriptExecuted) { continue; } @@ -335,7 +377,7 @@ export default class PoolCreateImpl extends PoolBaseImpl { try { //Delete scratchorgs that failed to execute script - let activeScratchOrgRecordId = await scratchOrgInfoFetcher.getActiveScratchOrgRecordIdGivenScratchOrg( + const activeScratchOrgRecordId = await scratchOrgInfoFetcher.getActiveScratchOrgRecordIdGivenScratchOrg( scratchOrg.orgId ); @@ -360,12 +402,12 @@ export default class PoolCreateImpl extends PoolBaseImpl { LoggerLevel.INFO ); - let startTime = Date.now(); - let result = await this.poolScriptExecutor.execute(scratchOrg, this.hubOrg, this.logLevel); + const startTime = Date.now(); + const result = await this.poolScriptExecutor.execute(scratchOrg, this.hubOrg, this.logLevel); if (result.isOk()) { scratchOrg.isScriptExecuted = true; - let submitInfoToPool = await this.scratchOrgInfoAssigner.setScratchOrgInfo({ + const submitInfoToPool = await this.scratchOrgInfoAssigner.setScratchOrgInfo({ Id: scratchOrg.recordId, Allocation_status__c: 'Available', }); diff --git a/packages/core/src/scratchorg/pool/PoolDeleteImpl.ts b/packages/core/src/scratchorg/pool/PoolDeleteImpl.ts index 122bb2059..ecc496643 100644 --- a/packages/core/src/scratchorg/pool/PoolDeleteImpl.ts +++ b/packages/core/src/scratchorg/pool/PoolDeleteImpl.ts @@ -4,6 +4,8 @@ import { PoolBaseImpl } from './PoolBaseImpl'; import ScratchOrg from '../ScratchOrg'; import ScratchOrgInfoFetcher from './services/fetchers/ScratchOrgInfoFetcher'; import ScratchOrgOperator from '../ScratchOrgOperator'; +import { Logger } from '@dxatscale/sfp-logger'; +import { LoggerLevel } from '@dxatscale/sfp-logger'; export default class PoolDeleteImpl extends PoolBaseImpl { private tag: string; @@ -11,7 +13,7 @@ export default class PoolDeleteImpl extends PoolBaseImpl { private allScratchOrgs: boolean; private inprogressonly: boolean; - public constructor(hubOrg: Org, tag: string, mypool: boolean, allScratchOrgs: boolean, inprogressonly: boolean) { + public constructor(hubOrg: Org, tag: string, mypool: boolean, allScratchOrgs: boolean, inprogressonly: boolean,private logger:Logger) { super(hubOrg); this.hubOrg = hubOrg; this.tag = tag; @@ -52,7 +54,7 @@ export default class PoolDeleteImpl extends PoolBaseImpl { if (activeScrathOrgs.records.length > 0) { for (let scratchOrg of activeScrathOrgs.records) { await new ScratchOrgOperator(this.hubOrg).delete(scratchOrg.Id); - SFPLogger.log(`Scratch org with username ${scratchOrg.SignupUsername} is deleted successfully`); + SFPLogger.log(`Scratch org with username ${scratchOrg.SignupUsername} is deleted successfully`,LoggerLevel.TRACE,this.logger); } } } diff --git a/packages/core/src/scratchorg/pool/services/fetchers/ScratchOrgInfoFetcher.ts b/packages/core/src/scratchorg/pool/services/fetchers/ScratchOrgInfoFetcher.ts index 5871cdbcb..bc45f1e01 100644 --- a/packages/core/src/scratchorg/pool/services/fetchers/ScratchOrgInfoFetcher.ts +++ b/packages/core/src/scratchorg/pool/services/fetchers/ScratchOrgInfoFetcher.ts @@ -47,9 +47,9 @@ export default class ScratchOrgInfoFetcher { let query; if (tag) - query = `SELECT Pooltag__c, Id, CreatedDate, ScratchOrg, ExpirationDate, SignupUsername, SignupEmail, Password__c, Allocation_status__c,LoginUrl,SfdxAuthUrl__c FROM ScratchOrgInfo WHERE Pooltag__c = '${tag}' AND Status = 'Active' `; + query = `SELECT Pooltag__c, Id, CreatedDate, ScratchOrg, ExpirationDate, SignupUsername, SignupEmail, Password__c, Allocation_status__c,LoginUrl,SfdxAuthUrl__c FROM ScratchOrgInfo WHERE Pooltag__c = '${tag}' AND Status = 'Active' `; else - query = `SELECT Pooltag__c, Id, CreatedDate, ScratchOrg, ExpirationDate, SignupUsername, SignupEmail, Password__c, Allocation_status__c,LoginUrl,SfdxAuthUrl__c FROM ScratchOrgInfo WHERE Pooltag__c != null AND Status = 'Active' `; + query = `SELECT Pooltag__c, Id, CreatedDate, ScratchOrg, ExpirationDate, SignupUsername, SignupEmail, Password__c, Allocation_status__c,LoginUrl,SfdxAuthUrl__c FROM ScratchOrgInfo WHERE Pooltag__c != null AND Status = 'Active' `; if (isMyPool) { query = query + ` AND createdby.username = '${this.hubOrg.getUsername()}' `; @@ -68,6 +68,22 @@ export default class ScratchOrgInfoFetcher { ); } + public async getOrphanedScratchOrgs() { + let hubConn = this.hubOrg.getConnection(); + + return retry( + async (bail) => { + let query; + query = `SELECT Id, Pooltag__c,SignupUsername,Description,ScratchOrg FROM ScratchOrgInfo WHERE Pooltag__c = null AND Status = 'Active'`; + query = query + ORDER_BY_FILTER; + SFPLogger.log('QUERY:' + query, LoggerLevel.TRACE); + const results = (await hubConn.query(query)) as any; + return results; + }, + { retries: 3, minTimeout: 3000 } + ); + } + public async getActiveScratchOrgsByInfoId(scrathOrgIds: string) { let hubConn = this.hubOrg.getConnection(); diff --git a/packages/sfpowerscripts-cli/messages/scratchorg_pooldelete.json b/packages/sfpowerscripts-cli/messages/pool_delete.json similarity index 68% rename from packages/sfpowerscripts-cli/messages/scratchorg_pooldelete.json rename to packages/sfpowerscripts-cli/messages/pool_delete.json index cb7f82ab5..1e558d567 100644 --- a/packages/sfpowerscripts-cli/messages/scratchorg_pooldelete.json +++ b/packages/sfpowerscripts-cli/messages/pool_delete.json @@ -3,5 +3,6 @@ "tagDescription": "tag used to identify the scratch org pool", "mypoolDescription": "Filter only Scratch orgs created by current user in the pool", "allscratchorgsDescription": "Deletes all used and unused Scratch orgs from pool by the tag", - "inprogressonlyDescription": "Deletes all In Progress Scratch orgs from pool by the tag" + "inprogressonlyDescription": "Deletes all In Progress Scratch orgs from pool by the tag", + "recoverOrphanedScratchOrgsDescription": "Recovers scratch orgs that were created by salesforce but were not tagged to sfpowerscripts due to timeouts etc." } diff --git a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/pool/delete.ts b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/pool/delete.ts index 055b3fc36..b7a4717b3 100644 --- a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/pool/delete.ts +++ b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/pool/delete.ts @@ -1,17 +1,23 @@ import { flags, SfdxCommand } from '@salesforce/command'; import { Messages } from '@salesforce/core'; -import { AnyJson } from '@salesforce/ts-types'; import PoolDeleteImpl from '@dxatscale/sfpowerscripts.core/lib/scratchorg/pool/PoolDeleteImpl'; +import OrphanedOrgsDeleteImpl from '@dxatscale/sfpowerscripts.core/lib/scratchorg/pool/OrphanedOrgsDeleteImpl'; import ScratchOrg from '@dxatscale/sfpowerscripts.core/lib/scratchorg/ScratchOrg'; +import SfpowerscriptsCommand from '../../../SfpowerscriptsCommand'; +import { ZERO_BORDER_TABLE } from '../../../ui/TableConstants'; +import SFPLogger, { ConsoleLogger, LoggerLevel } from '@dxatscale/sfp-logger'; +import { COLOR_KEY_MESSAGE } from '@dxatscale/sfp-logger'; +import { COLOR_WARNING } from '@dxatscale/sfp-logger'; +const Table = require('cli-table'); // Initialize Messages with the current plugin directory Messages.importMessagesDirectory(__dirname); // Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core, // or any library that is using the messages framework can also be loaded this way. -const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'scratchorg_pooldelete'); +const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'pool_delete'); -export default class Delete extends SfdxCommand { +export default class Delete extends SfpowerscriptsCommand { public static description = messages.getMessage('commandDescription'); protected static requiresDevhubUsername = true; @@ -19,17 +25,13 @@ export default class Delete extends SfdxCommand { public static examples = [ `$ sfdx sfpowerscripts:pool:delete -t core `, `$ sfdx sfpowerscripts:pool:delete -t core -v devhub`, + `$ sfdx sfpowerscripts:pool:delete --orphans -v devhub`, ]; protected static flagsConfig = { tag: flags.string({ char: 't', description: messages.getMessage('tagDescription'), - required: true, - }), - mypool: flags.boolean({ - char: 'm', - description: messages.getMessage('mypoolDescription'), required: false, }), allscratchorgs: flags.boolean({ @@ -43,6 +45,11 @@ export default class Delete extends SfdxCommand { required: false, exclusive: ['allscratchorgs'], }), + orphans: flags.boolean({ + char: 'o', + description: messages.getMessage('recoverOrphanedScratchOrgsDescription'), + required: false, + }), loglevel: flags.enum({ description: 'logging level for this command invocation', default: 'info', @@ -64,31 +71,70 @@ export default class Delete extends SfdxCommand { }), }; - public async run(): Promise { + public async execute(): Promise<{ orgId: string; username: string; operation: string }[]> { await this.hubOrg.refreshAuth(); const hubConn = this.hubOrg.getConnection(); this.flags.apiversion = this.flags.apiversion || (await hubConn.retrieveMaxApiVersion()); - let poolDeleteImpl = new PoolDeleteImpl( - this.hubOrg, - this.flags.tag, - this.flags.mypool, - this.flags.allscratchorgs, - this.flags.inprogressonly - ); + let scratchOrgOperationResults: { orgId: string; username: string; operation: string }[] = []; + //User want to delete orphans only + if (this.flags.orphans && !this.flags.tag) { + let orphanedOrgsDeleteImpl = new OrphanedOrgsDeleteImpl(this.hubOrg, new ConsoleLogger()); + let recoveredScratchOrgs = (await orphanedOrgsDeleteImpl.execute()) as ScratchOrg[]; + this.pushToResults('recovered', recoveredScratchOrgs, scratchOrgOperationResults); + } else { + let poolDeleteImpl = new PoolDeleteImpl( + this.hubOrg, + this.flags.tag, + this.flags.mypool, + this.flags.allscratchorgs, + this.flags.inprogressonly, + new ConsoleLogger() + ); - let result = (await poolDeleteImpl.execute()) as ScratchOrg[]; + let deletedOrgs = (await poolDeleteImpl.execute()) as ScratchOrg[]; + this.pushToResults('deleted', deletedOrgs, scratchOrgOperationResults); + + let orphanedOrgsDeleteImpl = new OrphanedOrgsDeleteImpl(this.hubOrg, new ConsoleLogger()); + let recoverdScratchOrgs = (await orphanedOrgsDeleteImpl.execute()) as ScratchOrg[]; + this.pushToResults('recovered', recoverdScratchOrgs, scratchOrgOperationResults); + } + this.displayScrathOrgOperationsAsTable(scratchOrgOperationResults); + return scratchOrgOperationResults; + } - if (!this.flags.json) { - if (result.length > 0) { - this.ux.log(`======== Scratch org Deleted ========`); - this.ux.table(result, ['orgId', 'username']); - } else { - console.log(`${this.flags.tag} pool has No Scratch orgs available to delete.`); - } + private pushToResults( + operation: string, + scratchOrgs: ScratchOrg[], + result: { orgId: string; username: string; operation: string }[] + ) { + for (const scratchOrg of scratchOrgs) { + result.push({ orgId: scratchOrg.orgId, username: scratchOrg.username, operation: operation }); } + } + + private displayScrathOrgOperationsAsTable( + scratchOrgOperationResults: { orgId: string; username: string; operation: string }[] + ) { + const table = new Table({ + head: ['Operation', 'OrgId', 'Username'], + chars: ZERO_BORDER_TABLE, + }); - return result as AnyJson; + if (scratchOrgOperationResults.length > 0) { + scratchOrgOperationResults.forEach((scratchOrgOperation) => { + table.push([COLOR_KEY_MESSAGE(scratchOrgOperation.operation), scratchOrgOperation.orgId, scratchOrgOperation.username]); + }); + + SFPLogger.log(`The command resulted in the following operation`, LoggerLevel.INFO, new ConsoleLogger()); + SFPLogger.log(table.toString(), LoggerLevel.INFO, new ConsoleLogger()); + } else { + SFPLogger.log( + `${COLOR_WARNING(`No Scratch Orgs were found to be operated upon, The command will now exist`)}`, + LoggerLevel.INFO, + new ConsoleLogger() + ); + } } }