@@ -105,7 +105,7 @@ export class DestroyOrchestrator {
105105 await DestroyOrchestrator . validateTypeIds ( matchedTypes , project , pluginManager , resourceDefinitions ) ;
106106
107107 const resourceInfoList = ( await pluginManager . getMultipleResourceInfo ( matchedTypes ) ) ;
108- const resourcesToDestroy = await DestroyOrchestrator . getDestroyParameters ( reporter , project , resourceInfoList ) ;
108+ const resourcesToDestroy = await DestroyOrchestrator . getDestroyParameters ( reporter , initializeResult , resourceInfoList ) ;
109109
110110 const destroyProject = new Project (
111111 null ,
@@ -187,30 +187,118 @@ ${JSON.stringify(unsupportedTypeIds)}`);
187187 }
188188 }
189189
190- private static async getDestroyParameters ( reporter : Reporter , project : Project , resourceInfoList : ResourceInfo [ ] ) : Promise < Array < ResourceConfig > > {
190+ /**
191+ * Resources that support multiple instances need identifying values before they can be destroyed (ex: which git
192+ * repository). Instead of asking for raw values, existing instances are found on the system (same as import all) and
193+ * the user picks one. The user can always choose to enter the values manually instead.
194+ */
195+ private static async getDestroyParameters (
196+ reporter : Reporter ,
197+ initializeResult : InitializationResult ,
198+ resourceInfoList : ResourceInfo [ ] ,
199+ ) : Promise < Array < ResourceConfig > > {
200+ const { project, pluginManager, resourceDefinitions } = initializeResult ;
201+
191202 // Figure out which resources we need to prompt the user for additional info (based on the resource info)
192203 const [ noPrompt , askPrompt ] = resourceInfoList . reduce ( ( result , info ) => {
193204 info . getRequiredParameters ( ) . length === 0 ? result [ 0 ] . push ( info ) : result [ 1 ] . push ( info ) ;
194205 return result ;
195206 } , [ < ResourceInfo [ ] > [ ] , < ResourceInfo [ ] > [ ] ] )
196207
197- askPrompt . forEach ( ( info ) => {
198- const matchedResources = project . findAll ( info . type ) ;
199- if ( matchedResources . length > 0 ) {
200- info . attachDefaultValues ( matchedResources [ 0 ] ) ;
208+ const existingInstances = askPrompt . length > 0
209+ ? await ctx . subprocess ( SubProcessName . FIND_EXISTING_RESOURCES , ( ) => Promise . all ( askPrompt . map ( ( info ) =>
210+ DestroyOrchestrator . findExistingInstances ( pluginManager , info , resourceDefinitions . get ( info . type ) ?. sensitiveParameters ?? [ ] )
211+ ) ) )
212+ : [ ] ;
213+
214+ const selected : ResourceConfig [ ] = [ ] ;
215+ const enterManually : ResourceInfo [ ] = [ ] ;
216+ for ( const [ idx , info ] of askPrompt . entries ( ) ) {
217+ const instances = existingInstances [ idx ] ;
218+ if ( instances . length === 0 ) {
219+ enterManually . push ( info ) ;
220+ continue ;
201221 }
202- } )
203222
204- if ( askPrompt . length > 0 ) {
205- await reporter . displayImportWarning ( askPrompt . map ( ( r ) => r . type ) , noPrompt . map ( ( r ) => r . type ) ) ;
223+ const choice = await reporter . promptOptions (
224+ `Multiple instances of "${ info . type } " can exist. Which one would you like to destroy?` ,
225+ [ ...instances . map ( ( i ) => i . label ) , 'Enter the values manually' ] ,
226+ ) ;
227+
228+ if ( choice < instances . length ) {
229+ selected . push ( instances [ choice ] . resource ) ;
230+ } else {
231+ enterManually . push ( info ) ;
232+ }
206233 }
207234
208- const userSupplied = await reporter . promptUserForValues ( askPrompt , PromptType . DESTROY ) ;
235+ let userSupplied : ResourceConfig [ ] = [ ] ;
236+ if ( enterManually . length > 0 ) {
237+ enterManually . forEach ( ( info ) => {
238+ const matchedResources = project . findAll ( info . type ) ;
239+ if ( matchedResources . length > 0 ) {
240+ info . attachDefaultValues ( matchedResources [ 0 ] ) ;
241+ }
242+ } )
243+
244+ await reporter . displayImportWarning ( enterManually . map ( ( r ) => r . type ) , noPrompt . map ( ( r ) => r . type ) ) ;
245+ userSupplied = await reporter . promptUserForValues ( enterManually , PromptType . DESTROY ) ;
246+ }
209247
210248 return [
211249 ...noPrompt . map ( ( info ) => new ResourceConfig ( { type : info . type } ) ) ,
212- ...userSupplied
250+ ...selected ,
251+ ...userSupplied ,
213252 ]
214253 }
215254
255+ /**
256+ * Find the instances of a resource that exist on the system using the plugin's import (the same call as import all).
257+ * Each instance is labelled by the values required to identify it. Returns an empty list if no instances are found.
258+ */
259+ private static async findExistingInstances (
260+ pluginManager : PluginManager ,
261+ info : ResourceInfo ,
262+ sensitiveParameters : string [ ] ,
263+ ) : Promise < Array < { label : string ; resource : ResourceConfig } > > {
264+ // Never list the values of a resource that is entirely sensitive
265+ if ( sensitiveParameters . includes ( '*' ) ) {
266+ return [ ] ;
267+ }
268+
269+ let imported : ResourceConfig [ ] ;
270+ try {
271+ const response = await pluginManager . importResource ( { core : { type : info . type } , parameters : { } } , true ) ;
272+ imported = ( response . result ?? [ ] ) . map ( ( r ) => ResourceConfig . fromJson ( r ) ) ;
273+ } catch ( error ) {
274+ ctx . debug ( `Unable to find existing instances of ${ info . type } : ${ error } ` ) ;
275+ return [ ] ;
276+ }
277+
278+ const requiredNames = info . getRequiredParameters ( ) . map ( ( p ) => p . name ) ;
279+ const seen = new Set < string > ( ) ;
280+ const labelCounts = new Map < string , number > ( ) ;
281+
282+ return imported
283+ // The imported instance is used as is: it contains every value the plugin needs to match it on the system
284+ . filter ( ( resource ) => {
285+ const key = JSON . stringify ( resource . parameters ) ;
286+ if ( seen . has ( key ) ) return false ;
287+ seen . add ( key ) ;
288+ return true ;
289+ } )
290+ . map ( ( resource ) => {
291+ const baseLabel = requiredNames
292+ . filter ( ( name ) => resource . parameters [ name ] !== undefined )
293+ . map ( ( name ) => `${ name } : ${ sensitiveParameters . includes ( name ) ? '********' : JSON . stringify ( resource . parameters [ name ] ) } ` )
294+ . join ( ', ' ) || resource . type ;
295+
296+ // Keep labels unique when instances only differ in values that aren't shown
297+ const count = ( labelCounts . get ( baseLabel ) ?? 0 ) + 1 ;
298+ labelCounts . set ( baseLabel , count ) ;
299+
300+ return { label : count > 1 ? `${ baseLabel } (${ count } )` : baseLabel , resource } ;
301+ } ) ;
302+ }
303+
216304}
0 commit comments