@@ -717,4 +717,211 @@ private void assertListEquals(List<String> first, List<String> second) {
717717 assertEquals (first .get (i ), second .get (i ));
718718 }
719719 }
720+
721+ // AC1: Uninstall script runs when component is deleted
722+ @ Test
723+ void GIVEN_component_with_uninstall_script_WHEN_component_removed_via_deployment_THEN_uninstall_script_executes ()
724+ throws Exception {
725+ CountDownLatch componentRunning = new CountDownLatch (1 );
726+ CountDownLatch componentUninstalling = new CountDownLatch (1 );
727+ CountDownLatch componentUninstalled = new CountDownLatch (1 );
728+ CountDownLatch uninstallScriptExecuted = new CountDownLatch (1 );
729+
730+ Consumer <GreengrassLogMessage > listener = m -> {
731+ if (m .getMessage () != null && m .getMessage ().contains ("Uninstall script executing" )) {
732+ uninstallScriptExecuted .countDown ();
733+ }
734+ };
735+
736+ try (AutoCloseable l = TestUtils .createCloseableLogListener (listener )) {
737+ kernel .getContext ().addGlobalStateChangeListener ((service , oldState , newState ) -> {
738+ if (service .getName ().equals ("UninstallTestComponent" )) {
739+ if (newState .equals (State .FINISHED )) {
740+ componentRunning .countDown ();
741+ } else if (newState .equals (State .UNINSTALLING )) {
742+ componentUninstalling .countDown ();
743+ } else if (newState .equals (State .UNINSTALLED )) {
744+ componentUninstalled .countDown ();
745+ }
746+ }
747+ });
748+
749+ submitSampleCloudDeploymentDocument (
750+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigWithUninstallTestComponent.json" )
751+ .toURI (), "DeployUninstallTestComponent" , DeploymentType .SHADOW );
752+ assertTrue (componentRunning .await (30 , TimeUnit .SECONDS ), "Component should reach FINISHED state" );
753+
754+ submitSampleCloudDeploymentDocument (
755+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigEmpty.json" ).toURI (),
756+ "RemoveAllComponents" , DeploymentType .SHADOW );
757+
758+ assertTrue (componentUninstalling .await (30 , TimeUnit .SECONDS ),
759+ "Component should reach UNINSTALLING state" );
760+ assertTrue (uninstallScriptExecuted .await (30 , TimeUnit .SECONDS ), "Uninstall script should execute" );
761+ assertTrue (componentUninstalled .await (30 , TimeUnit .SECONDS ),
762+ "Component should reach UNINSTALLED state" );
763+ }
764+ }
765+
766+ // AC2: Uninstall script failure doesn't fail deployment
767+ @ Test
768+ void GIVEN_component_with_failing_uninstall_script_WHEN_component_removed_THEN_deployment_succeeds ()
769+ throws Exception {
770+ CountDownLatch componentRunning = new CountDownLatch (1 );
771+ CountDownLatch componentUninstalled = new CountDownLatch (1 );
772+ CountDownLatch deploymentSucceeded = new CountDownLatch (1 );
773+
774+ DeploymentStatusKeeper deploymentStatusKeeper = kernel .getContext ().get (DeploymentStatusKeeper .class );
775+ deploymentStatusKeeper .registerDeploymentStatusConsumer (DeploymentType .SHADOW , (status ) -> {
776+ if (status .get (DEPLOYMENT_ID_KEY_NAME ).equals ("RemoveAllComponents" )
777+ && status .get (DEPLOYMENT_STATUS_KEY_NAME ).equals ("SUCCEEDED" )) {
778+ deploymentSucceeded .countDown ();
779+ }
780+ return true ;
781+ }, "UninstallFailureTest" );
782+
783+ kernel .getContext ().addGlobalStateChangeListener ((service , oldState , newState ) -> {
784+ if (service .getName ().equals ("FailingUninstallComponent" )) {
785+ if (newState .equals (State .FINISHED )) {
786+ componentRunning .countDown ();
787+ } else if (newState .equals (State .UNINSTALLED )) {
788+ componentUninstalled .countDown ();
789+ }
790+ }
791+ });
792+
793+ submitSampleCloudDeploymentDocument (
794+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigWithFailingUninstallComponent.json" )
795+ .toURI (), "DeployFailingUninstallComponent" , DeploymentType .SHADOW );
796+ assertTrue (componentRunning .await (30 , TimeUnit .SECONDS ), "Component should reach FINISHED state" );
797+
798+ submitSampleCloudDeploymentDocument (
799+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigEmpty.json" ).toURI (),
800+ "RemoveAllComponents" , DeploymentType .SHADOW );
801+
802+ assertTrue (componentUninstalled .await (30 , TimeUnit .SECONDS ),
803+ "Component should reach UNINSTALLED state despite script failure" );
804+ assertTrue (deploymentSucceeded .await (30 , TimeUnit .SECONDS ),
805+ "Deployment should succeed despite uninstall script failure" );
806+ }
807+
808+ // AC3: Uninstall script accesses environment variables
809+ @ Test
810+ void GIVEN_component_with_uninstall_script_WHEN_component_removed_THEN_environment_variables_accessible ()
811+ throws Exception {
812+ CountDownLatch componentRunning = new CountDownLatch (1 );
813+ CountDownLatch componentUninstalled = new CountDownLatch (1 );
814+ CountDownLatch envVarsLogged = new CountDownLatch (1 );
815+
816+ Consumer <GreengrassLogMessage > listener = m -> {
817+ if (m .getMessage () != null && m .getMessage ().contains ("AWS_GG_NUCLEUS_DOMAIN_SOCKET_FILEPATH" )) {
818+ envVarsLogged .countDown ();
819+ }
820+ };
821+
822+ try (AutoCloseable l = TestUtils .createCloseableLogListener (listener )) {
823+ kernel .getContext ().addGlobalStateChangeListener ((service , oldState , newState ) -> {
824+ if (service .getName ().equals ("UninstallWithEnvComponent" )) {
825+ if (newState .equals (State .FINISHED )) {
826+ componentRunning .countDown ();
827+ } else if (newState .equals (State .UNINSTALLED )) {
828+ componentUninstalled .countDown ();
829+ }
830+ }
831+ });
832+
833+ submitSampleCloudDeploymentDocument (
834+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigWithUninstallWithEnvComponent.json" )
835+ .toURI (), "DeployUninstallWithEnvComponent" , DeploymentType .SHADOW );
836+ assertTrue (componentRunning .await (30 , TimeUnit .SECONDS ), "Component should reach FINISHED state" );
837+
838+ submitSampleCloudDeploymentDocument (
839+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigEmpty.json" ).toURI (),
840+ "RemoveAllComponents" , DeploymentType .SHADOW );
841+
842+ assertTrue (envVarsLogged .await (30 , TimeUnit .SECONDS ),
843+ "Environment variables should be accessible in uninstall script" );
844+ assertTrue (componentUninstalled .await (30 , TimeUnit .SECONDS ),
845+ "Component should reach UNINSTALLED state" );
846+ }
847+ }
848+
849+ // AC4: Component lifecycle goes through UNINSTALLING and UNINSTALLED states
850+ @ Test
851+ void GIVEN_component_WHEN_component_removed_THEN_transitions_through_UNINSTALLING_to_UNINSTALLED ()
852+ throws Exception {
853+ CountDownLatch componentRunning = new CountDownLatch (1 );
854+ CountDownLatch componentUninstalling = new CountDownLatch (1 );
855+ CountDownLatch componentUninstalled = new CountDownLatch (1 );
856+
857+ kernel .getContext ().addGlobalStateChangeListener ((service , oldState , newState ) -> {
858+ if (service .getName ().equals ("UninstallTestComponent" )) {
859+ if (newState .equals (State .FINISHED )) {
860+ componentRunning .countDown ();
861+ } else if (newState .equals (State .UNINSTALLING )) {
862+ assertEquals (State .FINISHED , oldState , "Should transition from FINISHED to UNINSTALLING" );
863+ componentUninstalling .countDown ();
864+ } else if (newState .equals (State .UNINSTALLED )) {
865+ assertEquals (State .UNINSTALLING , oldState , "Should transition from UNINSTALLING to UNINSTALLED" );
866+ componentUninstalled .countDown ();
867+ }
868+ }
869+ });
870+
871+ submitSampleCloudDeploymentDocument (
872+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigWithUninstallTestComponent.json" )
873+ .toURI (), "DeployUninstallTestComponent" , DeploymentType .SHADOW );
874+ assertTrue (componentRunning .await (30 , TimeUnit .SECONDS ), "Component should reach FINISHED state" );
875+
876+ submitSampleCloudDeploymentDocument (
877+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigEmpty.json" ).toURI (),
878+ "RemoveAllComponents" , DeploymentType .SHADOW );
879+
880+ assertTrue (componentUninstalling .await (30 , TimeUnit .SECONDS ),
881+ "Component should transition to UNINSTALLING state" );
882+ assertTrue (componentUninstalled .await (30 , TimeUnit .SECONDS ),
883+ "Component should transition to UNINSTALLED state" );
884+ }
885+
886+
887+ // AC6: Uninstall timeout is configurable
888+ @ Test
889+ void GIVEN_component_with_custom_timeout_WHEN_component_removed_THEN_custom_timeout_respected ()
890+ throws Exception {
891+ CountDownLatch componentRunning = new CountDownLatch (1 );
892+ CountDownLatch componentUninstalled = new CountDownLatch (1 );
893+ CountDownLatch timeoutLogged = new CountDownLatch (1 );
894+
895+ Consumer <GreengrassLogMessage > listener = m -> {
896+ if (m .getMessage () != null && m .getContexts ().containsKey ("statusCode" ) && m .getContexts ().get ("statusCode" ).equals ("UNINSTALL_TIMEOUT" )) {
897+ timeoutLogged .countDown ();
898+ }
899+ };
900+
901+ try (AutoCloseable l = TestUtils .createCloseableLogListener (listener )) {
902+ kernel .getContext ().addGlobalStateChangeListener ((service , oldState , newState ) -> {
903+ if (service .getName ().equals ("CustomTimeoutUninstallComponent" )) {
904+ if (newState .equals (State .FINISHED )) {
905+ componentRunning .countDown ();
906+ } else if (newState .equals (State .UNINSTALLED )) {
907+ componentUninstalled .countDown ();
908+ }
909+ }
910+ });
911+
912+ submitSampleCloudDeploymentDocument (DeploymentServiceIntegrationTest .class
913+ .getResource ("FleetConfigWithCustomTimeoutUninstallComponent.json" ).toURI (),
914+ "DeployCustomTimeoutUninstallComponent" , DeploymentType .SHADOW );
915+ assertTrue (componentRunning .await (30 , TimeUnit .SECONDS ), "Component should reach FINISHED state" );
916+
917+ submitSampleCloudDeploymentDocument (
918+ DeploymentServiceIntegrationTest .class .getResource ("FleetConfigEmpty.json" ).toURI (),
919+ "RemoveAllComponents" , DeploymentType .SHADOW );
920+
921+ assertTrue (timeoutLogged .await (15 , TimeUnit .SECONDS ),
922+ "Uninstall script should timeout with custom timeout (10s)" );
923+ assertTrue (componentUninstalled .await (30 , TimeUnit .SECONDS ),
924+ "Component should reach UNINSTALLED state after timeout" );
925+ }
926+ }
720927}
0 commit comments