@@ -145,27 +145,81 @@ pub fn plan(capabilities: &ProjectCapabilities, runtime: &RuntimeCapabilities) -
145145 DeploymentPlan { steps, blockers }
146146}
147147
148+ pub trait CommandRunner {
149+ fn run (
150+ & self ,
151+ cmd : & str ,
152+ args : & [ & str ] ,
153+ current_dir : Option < & std:: path:: Path > ,
154+ ) -> Result < std:: process:: Output > ;
155+ }
156+
157+ pub struct RealCommandRunner ;
158+
159+ impl CommandRunner for RealCommandRunner {
160+ fn run (
161+ & self ,
162+ cmd : & str ,
163+ args : & [ & str ] ,
164+ current_dir : Option < & std:: path:: Path > ,
165+ ) -> Result < std:: process:: Output > {
166+ let mut command = Command :: new ( cmd) ;
167+ command. args ( args) ;
168+ if let Some ( dir) = current_dir {
169+ command. current_dir ( dir) ;
170+ }
171+ command
172+ . output ( )
173+ . context ( format ! ( "Failed to execute {}" , cmd) )
174+ }
175+ }
176+
148177/// Execute the deployment pipeline against a real project.
149178pub fn execute_pipeline (
150179 plan : & DeploymentPlan ,
151180 project : & ProjectContext ,
152181 capabilities : & ProjectCapabilities ,
182+ environment_str : & str ,
183+ ) -> Result < PipelineExecution > {
184+ execute_pipeline_with_runner (
185+ plan,
186+ project,
187+ capabilities,
188+ environment_str,
189+ & RealCommandRunner ,
190+ )
191+ }
192+
193+ pub fn execute_pipeline_with_runner (
194+ plan : & DeploymentPlan ,
195+ project : & ProjectContext ,
196+ capabilities : & ProjectCapabilities ,
197+ environment_str : & str ,
198+ runner : & dyn CommandRunner ,
153199) -> Result < PipelineExecution > {
154200 if !plan. ready ( ) {
155201 anyhow:: bail!( "Deployment plan has blockers: {}" , plan. blockers. join( ", " ) ) ;
156202 }
157203
204+ let env = crate :: deploy:: environments:: from_string ( environment_str) ;
205+ let namespace = crate :: deploy:: environments:: resolve_namespace ( & env) ;
206+
158207 let mut results = Vec :: new ( ) ;
159208 let mut overall_success = true ;
160209
161210 for step in & plan. steps {
162211 let start = Instant :: now ( ) ;
163212 let step_result = match step {
164- PipelineStep :: Build => execute_build_step ( project) ,
213+ PipelineStep :: Build => execute_build_step ( project, runner ) ,
165214 PipelineStep :: DockerBuild => execute_docker_build_step ( project) ,
166215 PipelineStep :: DockerPush => execute_docker_push_step ( project) ,
167- PipelineStep :: DeploymentUpdate => execute_deployment_update_step ( project, capabilities) ,
168- PipelineStep :: RolloutVerification => execute_rollout_verification_step ( ) ,
216+ PipelineStep :: DeploymentUpdate => {
217+ execute_deployment_update_step ( project, capabilities, & namespace, runner)
218+ }
219+ PipelineStep :: RolloutVerification => {
220+ let deployment_name = project. name . to_lowercase ( ) . replace ( ' ' , "-" ) ;
221+ execute_rollout_verification_step ( & deployment_name, & namespace, runner)
222+ }
169223 } ;
170224 let duration_secs = start. elapsed ( ) . as_secs_f64 ( ) ;
171225
@@ -198,7 +252,7 @@ pub fn execute_pipeline(
198252 } )
199253}
200254
201- fn execute_build_step ( project : & ProjectContext ) -> Result < String > {
255+ fn execute_build_step ( project : & ProjectContext , runner : & dyn CommandRunner ) -> Result < String > {
202256 let build_cmd =
203257 crate :: templates:: stacks:: build_command ( project. stack ) . unwrap_or ( "echo 'No build step'" ) ;
204258
@@ -207,11 +261,7 @@ fn execute_build_step(project: &ProjectContext) -> Result<String> {
207261 return Ok ( "No build command for this stack" . to_string ( ) ) ;
208262 }
209263
210- let output = Command :: new ( parts[ 0 ] )
211- . args ( & parts[ 1 ..] )
212- . current_dir ( & project. root )
213- . output ( )
214- . with_context ( || format ! ( "Failed to execute build command: {build_cmd}" ) ) ?;
264+ let output = runner. run ( parts[ 0 ] , & parts[ 1 ..] , Some ( & project. root ) ) ?;
215265
216266 if output. status . success ( ) {
217267 Ok ( format ! ( "Build completed: {build_cmd}" ) )
@@ -247,23 +297,25 @@ fn execute_docker_push_step(project: &ProjectContext) -> Result<String> {
247297fn execute_deployment_update_step (
248298 project : & ProjectContext ,
249299 capabilities : & ProjectCapabilities ,
300+ namespace : & str ,
301+ runner : & dyn CommandRunner ,
250302) -> Result < String > {
251303 if !capabilities. kubernetes {
252304 return Ok ( "No Kubernetes manifests to apply" . to_string ( ) ) ;
253305 }
254306
255- // Apply all detected Kubernetes manifests.
256307 let k8s_dir = project. root . join ( "k8s" ) ;
257- let manifest_path = if k8s_dir. exists ( ) {
258- k8s_dir. to_string_lossy ( ) . to_string ( )
259- } else {
260- project. root . to_string_lossy ( ) . to_string ( )
261- } ;
308+ if !k8s_dir. exists ( ) || !k8s_dir. is_dir ( ) {
309+ anyhow:: bail!( "k8s/ directory is absent" ) ;
310+ }
262311
263- let output = Command :: new ( "kubectl" )
264- . args ( [ "apply" , "-f" , & manifest_path] )
265- . output ( )
266- . context ( "Failed to execute kubectl apply" ) ?;
312+ let manifest_path = k8s_dir. to_string_lossy ( ) . to_string ( ) ;
313+
314+ let output = runner. run (
315+ "kubectl" ,
316+ & [ "apply" , "-f" , & manifest_path, "-n" , namespace] ,
317+ None ,
318+ ) ?;
267319
268320 if output. status . success ( ) {
269321 let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
@@ -274,11 +326,23 @@ fn execute_deployment_update_step(
274326 }
275327}
276328
277- fn execute_rollout_verification_step ( ) -> Result < String > {
278- let output = Command :: new ( "kubectl" )
279- . args ( [ "rollout" , "status" , "deployment" , "--timeout=120s" ] )
280- . output ( )
281- . context ( "Failed to execute kubectl rollout status" ) ?;
329+ fn execute_rollout_verification_step (
330+ name : & str ,
331+ namespace : & str ,
332+ runner : & dyn CommandRunner ,
333+ ) -> Result < String > {
334+ let output = runner. run (
335+ "kubectl" ,
336+ & [
337+ "rollout" ,
338+ "status" ,
339+ & format ! ( "deployment/{}" , name) ,
340+ "-n" ,
341+ namespace,
342+ "--timeout=120s" ,
343+ ] ,
344+ None ,
345+ ) ?;
282346
283347 if output. status . success ( ) {
284348 Ok ( "Rollout verified successfully" . to_string ( ) )
@@ -362,3 +426,130 @@ mod tests {
362426 assert ! ( rendered. contains( "✗ Docker Build" ) ) ;
363427 }
364428}
429+
430+ #[ cfg( test) ]
431+ mod pipeline_mock_tests {
432+ use super :: * ;
433+ use std:: sync:: Mutex ;
434+
435+ struct MockRunner {
436+ calls : Mutex < Vec < ( String , Vec < String > ) > > ,
437+ success : bool ,
438+ }
439+
440+ impl CommandRunner for MockRunner {
441+ fn run (
442+ & self ,
443+ cmd : & str ,
444+ args : & [ & str ] ,
445+ _current_dir : Option < & std:: path:: Path > ,
446+ ) -> Result < std:: process:: Output > {
447+ self . calls . lock ( ) . unwrap ( ) . push ( (
448+ cmd. to_string ( ) ,
449+ args. iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ,
450+ ) ) ;
451+ let status = if self . success {
452+ Command :: new ( "true" ) . status ( ) . unwrap ( )
453+ } else {
454+ Command :: new ( "false" ) . status ( ) . unwrap ( )
455+ } ;
456+ Ok ( std:: process:: Output {
457+ status,
458+ stdout : b"mock-output" . to_vec ( ) ,
459+ stderr : b"mock-error" . to_vec ( ) ,
460+ } )
461+ }
462+ }
463+
464+ #[ test]
465+ fn test_execute_deployment_update_step ( ) {
466+ let temp = std:: env:: temp_dir ( ) . join ( format ! (
467+ "kdc-k8s-test-{}" ,
468+ std:: time:: SystemTime :: now( )
469+ . duration_since( std:: time:: UNIX_EPOCH )
470+ . unwrap( )
471+ . as_nanos( )
472+ ) ) ;
473+ std:: fs:: create_dir_all ( temp. join ( "k8s" ) ) . unwrap ( ) ;
474+
475+ let project = ProjectContext {
476+ name : "test-proj" . to_string ( ) ,
477+ root : temp. clone ( ) ,
478+ stack : crate :: domain:: project:: ProjectStack :: Rust ,
479+ assets : vec ! [ ] ,
480+ } ;
481+ let caps = ProjectCapabilities {
482+ kubernetes : true ,
483+ ..Default :: default ( )
484+ } ;
485+ let runner = MockRunner {
486+ calls : Mutex :: new ( vec ! [ ] ) ,
487+ success : true ,
488+ } ;
489+
490+ let result = execute_deployment_update_step ( & project, & caps, "my-namespace" , & runner) ;
491+ assert ! ( result. is_ok( ) ) ;
492+
493+ let calls = runner. calls . lock ( ) . unwrap ( ) ;
494+ assert_eq ! ( calls. len( ) , 1 ) ;
495+ assert_eq ! ( calls[ 0 ] . 0 , "kubectl" ) ;
496+ assert ! ( calls[ 0 ] . 1 . contains( & "-n" . to_string( ) ) ) ;
497+ assert ! ( calls[ 0 ] . 1 . contains( & "my-namespace" . to_string( ) ) ) ;
498+ assert ! ( calls[ 0 ]
499+ . 1
500+ . contains( & temp. join( "k8s" ) . to_string_lossy( ) . to_string( ) ) ) ;
501+
502+ std:: fs:: remove_dir_all ( temp) . unwrap ( ) ;
503+ }
504+
505+ #[ test]
506+ fn test_execute_deployment_update_step_missing_k8s ( ) {
507+ let temp = std:: env:: temp_dir ( ) . join ( format ! (
508+ "kdc-k8s-test-{}" ,
509+ std:: time:: SystemTime :: now( )
510+ . duration_since( std:: time:: UNIX_EPOCH )
511+ . unwrap( )
512+ . as_nanos( )
513+ ) ) ;
514+ std:: fs:: create_dir_all ( & temp) . unwrap ( ) ; // no k8s folder
515+
516+ let project = ProjectContext {
517+ name : "test-proj" . to_string ( ) ,
518+ root : temp. clone ( ) ,
519+ stack : crate :: domain:: project:: ProjectStack :: Rust ,
520+ assets : vec ! [ ] ,
521+ } ;
522+ let caps = ProjectCapabilities {
523+ kubernetes : true ,
524+ ..Default :: default ( )
525+ } ;
526+ let runner = MockRunner {
527+ calls : Mutex :: new ( vec ! [ ] ) ,
528+ success : true ,
529+ } ;
530+
531+ let result = execute_deployment_update_step ( & project, & caps, "my-namespace" , & runner) ;
532+ assert ! ( result. is_err( ) ) ;
533+ assert_eq ! ( result. unwrap_err( ) . to_string( ) , "k8s/ directory is absent" ) ;
534+
535+ std:: fs:: remove_dir_all ( temp) . unwrap ( ) ;
536+ }
537+
538+ #[ test]
539+ fn test_execute_rollout_verification_step ( ) {
540+ let runner = MockRunner {
541+ calls : Mutex :: new ( vec ! [ ] ) ,
542+ success : true ,
543+ } ;
544+
545+ let result = execute_rollout_verification_step ( "my-app" , "my-namespace" , & runner) ;
546+ assert ! ( result. is_ok( ) ) ;
547+
548+ let calls = runner. calls . lock ( ) . unwrap ( ) ;
549+ assert_eq ! ( calls. len( ) , 1 ) ;
550+ assert_eq ! ( calls[ 0 ] . 0 , "kubectl" ) ;
551+ assert ! ( calls[ 0 ] . 1 . contains( & "deployment/my-app" . to_string( ) ) ) ;
552+ assert ! ( calls[ 0 ] . 1 . contains( & "-n" . to_string( ) ) ) ;
553+ assert ! ( calls[ 0 ] . 1 . contains( & "my-namespace" . to_string( ) ) ) ;
554+ }
555+ }
0 commit comments