From 06f4fcdc53a384d6a9c85e68b7debf10dfbe25a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C3=ABl=20Minelli?= <michael@minelli.me>
Date: Fri, 12 Jan 2024 18:05:40 +0100
Subject: [PATCH] ExerciseDockerCompose => Add dangling image removing

---
 helpers/Dojo/ExerciseDockerCompose.ts | 93 +++++++++++++++------------
 1 file changed, 52 insertions(+), 41 deletions(-)

diff --git a/helpers/Dojo/ExerciseDockerCompose.ts b/helpers/Dojo/ExerciseDockerCompose.ts
index 058c9e9..8d766aa 100644
--- a/helpers/Dojo/ExerciseDockerCompose.ts
+++ b/helpers/Dojo/ExerciseDockerCompose.ts
@@ -1,8 +1,9 @@
-import AssignmentFile        from '../../../shared/types/Dojo/AssignmentFile';
-import { TypedEmitter }      from 'tiny-typed-emitter';
-import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents';
-import { spawn }             from 'child_process';
-import ExerciseCheckerError  from '../../../shared/types/Dojo/ExerciseCheckerError';
+import AssignmentFile                     from '../../../shared/types/Dojo/AssignmentFile';
+import { TypedEmitter }                   from 'tiny-typed-emitter';
+import ExerciseRunningEvents              from '../../types/Dojo/ExerciseRunningEvents';
+import { spawn }                          from 'child_process';
+import ExerciseCheckerError               from '../../../shared/types/Dojo/ExerciseCheckerError';
+import { ChildProcessWithoutNullStreams } from 'node:child_process';
 
 
 class ExerciseDockerCompose {
@@ -28,6 +29,20 @@ class ExerciseDockerCompose {
         });
     }
 
+    private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void) {
+        childProcess.stdout.on('data', (data) => {
+            this.events.emit('logs', data.toString(), false, false);
+        });
+
+        childProcess.stderr.on('data', (data) => {
+            this.events.emit('logs', data.toString(), true, false);
+        });
+
+        childProcess.on('exit', (code) => {
+            code !== null ? resolve(code) : reject();
+        });
+    }
+
     run(doDown: boolean = false) {
         (async () => {
             let containerExitCode: number = -1;
@@ -52,17 +67,7 @@ class ExerciseDockerCompose {
                             }
                         });
 
-                        dockerCompose.stdout.on('data', (data) => {
-                            this.events.emit('logs', data.toString(), false, true);
-                        });
-
-                        dockerCompose.stderr.on('data', (data) => {
-                            this.events.emit('logs', data.toString(), true, true);
-                        });
-
-                        dockerCompose.on('exit', (code) => {
-                            code !== null ? resolve(code) : reject();
-                        });
+                        this.registerChildProcess(dockerCompose, resolve, reject);
                     });
                 } catch ( error ) {
                     this.events.emit('endStep', 'COMPOSE_RUN', `Error while running the docker compose file`, true);
@@ -77,7 +82,7 @@ class ExerciseDockerCompose {
                 try {
                     this.events.emit('step', 'COMPOSE_LOGS', 'Linked services logs acquisition');
 
-                    await new Promise<void>((resolve, reject) => {
+                    await new Promise<number>((resolve, reject) => {
 
                         this.events.emit('logs', '####################################################### Other Services Logs #######################################################\n', false, false);
 
@@ -86,17 +91,7 @@ class ExerciseDockerCompose {
                             shell: true
                         });
 
-                        dockerCompose.stdout.on('data', (data) => {
-                            this.events.emit('logs', data.toString(), false, false);
-                        });
-
-                        dockerCompose.stderr.on('data', (data) => {
-                            this.events.emit('logs', data.toString(), true, false);
-                        });
-
-                        dockerCompose.on('exit', (code) => {
-                            code !== null ? resolve() : reject();
-                        });
+                        this.registerChildProcess(dockerCompose, resolve, reject);
                     });
                 } catch ( error ) {
                     this.events.emit('endStep', 'COMPOSE_LOGS', `Error while getting the linked services logs`, true);
@@ -112,26 +107,16 @@ class ExerciseDockerCompose {
                     try {
                         this.events.emit('step', 'COMPOSE_DOWN', 'Stopping and removing containers');
 
-                        await new Promise<void>((resolve, reject) => {
+                        await new Promise<number>((resolve, reject) => {
 
                             this.events.emit('logs', '####################################################### Stop and remove containers #######################################################\n', false, false);
 
-                            const dockerCompose = spawn(`${ dockerComposeCommand } down --volumes`, {
+                            const dockerCompose = spawn(`${ dockerComposeCommand } down --volumes --rmi`, {
                                 cwd  : this.executionFolder,
                                 shell: true
                             });
 
-                            dockerCompose.stdout.on('data', (data) => {
-                                this.events.emit('logs', data.toString(), false, false);
-                            });
-
-                            dockerCompose.stderr.on('data', (data) => {
-                                this.events.emit('logs', data.toString(), true, false);
-                            });
-
-                            dockerCompose.on('exit', (code) => {
-                                code !== null ? resolve() : reject();
-                            });
+                            this.registerChildProcess(dockerCompose, resolve, reject);
                         });
                     } catch ( error ) {
                         this.events.emit('endStep', 'COMPOSE_DOWN', `Error stop and remove containers`, true);
@@ -142,6 +127,32 @@ class ExerciseDockerCompose {
                 }
             }
 
+            // Remove images if asked
+            {
+                if ( doDown ) {
+                    try {
+                        this.events.emit('step', 'COMPOSE_REMOVE_DANGLING', 'Removing dangling images');
+
+                        await new Promise<number>((resolve, reject) => {
+
+                            this.events.emit('logs', '####################################################### Remove dangling images #######################################################\n', false, false);
+
+                            const dockerCompose = spawn(`docker image prune --force`, {
+                                cwd  : this.executionFolder,
+                                shell: true
+                            });
+
+                            this.registerChildProcess(dockerCompose, resolve, reject);
+                        });
+                    } catch ( error ) {
+                        this.events.emit('endStep', 'COMPOSE_REMOVE_DANGLING', `Error while removing dangling images`, true);
+                        this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_REMOVE_DANGLING_ERROR);
+                        return;
+                    }
+                    this.events.emit('endStep', 'COMPOSE_REMOVE_DANGLING', `Dangling images removed`, false);
+                }
+            }
+
             this.events.emit('finished', true, containerExitCode);
         })();
     }
-- 
GitLab