11import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js" ;
22import { stackFlags , withStackId } from "../../lib/resources/stack/flags.js" ;
33import { ReactNode } from "react" ;
4- import { Flags } from "@oclif/core" ;
4+ import { Flags , ux } from "@oclif/core" ;
55import {
66 makeProcessRenderer ,
77 processFlags ,
@@ -19,13 +19,13 @@ import {
1919import { sanitizeStackDefinition } from "../../lib/resources/stack/sanitize.js" ;
2020import { enrichStackDefinition } from "../../lib/resources/stack/enrich.js" ;
2121import { Success } from "../../rendering/react/components/Success.js" ;
22- import { Value } from "../../rendering/react/components/Value.js" ;
2322import { loadStackFromTemplate } from "../../lib/resources/stack/template-loader.js" ;
2423import { parseEnvironmentVariablesFromStr } from "../../lib/util/parser.js" ;
2524import { RawStackInput } from "../../lib/resources/stack/types.js" ;
2625
2726interface DeployResult {
2827 restartedServices : string [ ] ;
28+ deletedServices : string [ ] ;
2929}
3030
3131type StackRequest =
@@ -65,8 +65,106 @@ This flag is mutually exclusive with --compose-file.`,
6565 summary : "alternative path to file with environment variables" ,
6666 default : "./.env" ,
6767 } ) ,
68+ force : Flags . boolean ( {
69+ char : "f" ,
70+ summary : "do not ask for confirmation when containers will be deleted" ,
71+ } ) ,
6872 } ;
6973
74+ private findServicesToDelete (
75+ existingStack : ContainerStackResponse ,
76+ newStackDefinition : RawStackInput ,
77+ ) : string [ ] {
78+ const existingServiceNames = ( existingStack . services ?? [ ] ) . map (
79+ ( s ) => s . serviceName ,
80+ ) ;
81+ const newServiceNames = Object . keys ( newStackDefinition . services ?? { } ) ;
82+
83+ return existingServiceNames . filter (
84+ ( name ) => ! newServiceNames . includes ( name ) ,
85+ ) ;
86+ }
87+
88+ private async getExistingStack (
89+ stackId : string ,
90+ renderer : ReturnType < typeof makeProcessRenderer > ,
91+ ) : Promise < ContainerStackResponse > {
92+ return renderer . runStep ( "retrieving current stack state" , async ( ) => {
93+ const resp = await this . apiClient . container . getStack ( { stackId } ) ;
94+ assertStatus ( resp , 200 ) ;
95+ return resp . data ;
96+ } ) ;
97+ }
98+
99+ private async confirmDeletion (
100+ servicesToDelete : string [ ] ,
101+ renderer : ReturnType < typeof makeProcessRenderer > ,
102+ ) : Promise < boolean > {
103+ if ( servicesToDelete . length === 0 ) {
104+ return true ;
105+ }
106+
107+ renderer . addInfo (
108+ `the following containers will be deleted: ${ servicesToDelete . join ( ", " ) } ` ,
109+ ) ;
110+
111+ if ( this . flags . force ) {
112+ return true ;
113+ }
114+
115+ const confirmed = await renderer . addConfirmation (
116+ "do you want to continue and delete these containers?" ,
117+ ) ;
118+
119+ if ( ! confirmed ) {
120+ await renderer . error ( "deployment cancelled by user" ) ;
121+ ux . exit ( 1 ) ;
122+ }
123+
124+ return confirmed ;
125+ }
126+
127+ private async deployStack (
128+ stackId : string ,
129+ stackDefinition : RawStackInput ,
130+ renderer : ReturnType < typeof makeProcessRenderer > ,
131+ ) : Promise < ContainerStackResponse > {
132+ return renderer . runStep ( "deploying stack" , async ( ) => {
133+ const resp = await this . apiClient . container . declareStack ( {
134+ stackId,
135+ data : stackDefinition as StackRequest ,
136+ } ) ;
137+ assertStatus ( resp , 200 ) ;
138+ return resp . data ;
139+ } ) ;
140+ }
141+
142+ private async recreateServices (
143+ stackId : string ,
144+ declaredStack : ContainerStackResponse ,
145+ renderer : ReturnType < typeof makeProcessRenderer > ,
146+ ) : Promise < string [ ] > {
147+ const restartedServices : string [ ] = [ ] ;
148+
149+ for ( const service of declaredStack . services ?? [ ] ) {
150+ if ( service . requiresRecreate ) {
151+ await renderer . runStep (
152+ `recreating service ${ service . serviceName } ` ,
153+ async ( ) => {
154+ const resp = await this . apiClient . container . recreateService ( {
155+ stackId,
156+ serviceId : service . id ,
157+ } ) ;
158+ assertSuccess ( resp ) ;
159+ restartedServices . push ( service . serviceName ) ;
160+ } ,
161+ ) ;
162+ }
163+ }
164+
165+ return restartedServices ;
166+ }
167+
70168 private async loadStackDefinition (
71169 source : { template : string } | { composeFile : string } ,
72170 envFile : string ,
@@ -122,20 +220,13 @@ This flag is mutually exclusive with --compose-file.`,
122220 } = this . flags ;
123221 const r = makeProcessRenderer ( this . flags , "Deploying container stack" ) ;
124222
125- const existingStack = await r . runStep (
126- "retrieving current stack state" ,
127- async ( ) => {
128- const resp = await this . apiClient . container . getStack ( { stackId } ) ;
129- assertStatus ( resp , 200 ) ;
130-
131- return resp . data ;
132- } ,
133- ) ;
134-
135- const result : DeployResult = { restartedServices : [ ] } ;
223+ const existingStack = await this . getExistingStack ( stackId , r ) ;
224+ const stackSource = fromTemplate
225+ ? { template : fromTemplate }
226+ : { composeFile } ;
136227
137228 let stackDefinition = await this . loadStackDefinition (
138- fromTemplate ? { template : fromTemplate } : { composeFile } ,
229+ stackSource ,
139230 envFile ,
140231 existingStack ,
141232 r ,
@@ -146,46 +237,38 @@ This flag is mutually exclusive with --compose-file.`,
146237 enrichStackDefinition ( stackDefinition ) ,
147238 ) ;
148239
149- const declaredStack = await r . runStep ( "deploying stack" , async ( ) => {
150- const resp = await this . apiClient . container . declareStack ( {
151- stackId,
152- data : stackDefinition as StackRequest ,
153- } ) ;
154-
155- assertStatus ( resp , 200 ) ;
156- return resp . data ;
157- } ) ;
158-
159- for ( const service of declaredStack . services ?? [ ] ) {
160- if ( service . requiresRecreate ) {
161- await r . runStep (
162- `recreating service ${ service . serviceName } ` ,
163- async ( ) => {
164- const resp = await this . apiClient . container . recreateService ( {
165- stackId,
166- serviceId : service . id ,
167- } ) ;
168- assertSuccess ( resp ) ;
169- result . restartedServices . push ( service . serviceName ) ;
170- } ,
171- ) ;
172- }
240+ const servicesToDelete = this . findServicesToDelete (
241+ existingStack ,
242+ stackDefinition ,
243+ ) ;
244+ const confirmed = await this . confirmDeletion ( servicesToDelete , r ) ;
245+ if ( ! confirmed ) {
246+ return { restartedServices : [ ] , deletedServices : [ ] } ;
173247 }
174248
175- return result ;
176- }
249+ const declaredStack = await this . deployStack ( stackId , stackDefinition , r ) ;
250+ const restartedServices = await this . recreateServices (
251+ stackId ,
252+ declaredStack ,
253+ r ,
254+ ) ;
177255
178- protected render ( { restartedServices } : DeployResult ) : ReactNode {
179- if ( restartedServices . length === 0 ) {
180- return (
181- < Success > Deployment successful. No services were restarted.</ Success >
182- ) ;
183- }
256+ return { restartedServices, deletedServices : servicesToDelete } ;
257+ }
184258
259+ protected render ( {
260+ restartedServices,
261+ deletedServices,
262+ } : DeployResult ) : ReactNode {
185263 return (
186264 < Success >
187- Deployment successful. The following services were restarted:{ " " }
188- < Value > { restartedServices . join ( ", " ) } </ Value >
265+ Deployment successful.{ " " }
266+ { restartedServices . length > 0
267+ ? `The following services were restarted: ${ restartedServices . join ( ", " ) } `
268+ : "No services were restarted." } { " " }
269+ { deletedServices . length > 0
270+ ? `The following services were deleted: ${ deletedServices . join ( ", " ) } `
271+ : "No services were deleted." }
189272 </ Success >
190273 ) ;
191274 }
0 commit comments