Skip to content

Commit e0768ab

Browse files
committed
feat: add uninstall lifecycle
1 parent b0b767b commit e0768ab

23 files changed

+951
-11
lines changed

src/integrationtests/java/com/aws/greengrass/integrationtests/deployment/DeploymentServiceIntegrationTest.java

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/integrationtests/java/com/aws/greengrass/integrationtests/lifecyclemanager/GenericExternalServiceIntegTest.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,97 @@ void GIVEN_running_service_WHEN_pause_resume_requested_THEN_pause_resume_Service
884884
assertFalse(freezerManager.isComponentFrozen(component.getServiceName()));
885885
}
886886

887+
// AC1 & AC3: End-to-end uninstall with environment variables
888+
@Test
889+
void GIVEN_component_with_uninstall_script_WHEN_uninstall_THEN_script_executes_successfully() throws Exception {
890+
ConfigPlatformResolver.initKernelWithMultiPlatformConfig(kernel,
891+
getClass().getResource("uninstall_success_config.yaml"));
892+
893+
CountDownLatch finishedLatch = new CountDownLatch(1);
894+
CountDownLatch uninstalledLatch = new CountDownLatch(1);
895+
896+
kernel.getContext().addGlobalStateChangeListener((service, oldState, newState) -> {
897+
if (service.getName().equals("UninstallTestComponent")) {
898+
if (newState.equals(State.FINISHED)) {
899+
finishedLatch.countDown();
900+
} else if (newState.equals(State.UNINSTALLED)) {
901+
uninstalledLatch.countDown();
902+
}
903+
}
904+
});
905+
906+
kernel.launch();
907+
assertTrue(finishedLatch.await(30, TimeUnit.SECONDS), "component should reach FINISHED");
908+
909+
GenericExternalService component = (GenericExternalService) kernel.locate("UninstallTestComponent");
910+
component.requestUninstall();
911+
912+
assertTrue(uninstalledLatch.await(30, TimeUnit.SECONDS), "component should reach UNINSTALLED");
913+
assertThat(component.getState(), is(State.UNINSTALLED));
914+
}
915+
916+
// AC2: Uninstall script failure doesn't fail deployment
917+
@Test
918+
void GIVEN_component_with_failing_uninstall_script_WHEN_uninstall_THEN_component_still_uninstalled() throws Exception {
919+
ConfigPlatformResolver.initKernelWithMultiPlatformConfig(kernel,
920+
getClass().getResource("uninstall_failure_config.yaml"));
921+
922+
CountDownLatch finishedLatch = new CountDownLatch(1);
923+
CountDownLatch uninstalledLatch = new CountDownLatch(1);
924+
925+
kernel.getContext().addGlobalStateChangeListener((service, oldState, newState) -> {
926+
if (service.getName().equals("FailingUninstallComponent")) {
927+
if (newState.equals(State.FINISHED)) {
928+
finishedLatch.countDown();
929+
} else if (newState.equals(State.UNINSTALLED)) {
930+
uninstalledLatch.countDown();
931+
}
932+
}
933+
});
934+
935+
kernel.launch();
936+
assertTrue(finishedLatch.await(30, TimeUnit.SECONDS), "component should reach FINISHED");
937+
938+
GenericExternalService component = (GenericExternalService) kernel.locate("FailingUninstallComponent");
939+
component.requestUninstall();
940+
941+
assertTrue(uninstalledLatch.await(30, TimeUnit.SECONDS), "component should reach UNINSTALLED despite script failure");
942+
assertThat(component.getState(), is(State.UNINSTALLED));
943+
}
944+
945+
// AC7: State transitions
946+
@Test
947+
void GIVEN_component_WHEN_uninstall_requested_THEN_goes_through_UNINSTALLING_to_UNINSTALLED_states() throws Exception {
948+
ConfigPlatformResolver.initKernelWithMultiPlatformConfig(kernel,
949+
getClass().getResource("uninstall_state_transition_config.yaml"));
950+
951+
List<State> stateTransitions = new CopyOnWriteArrayList<>();
952+
CountDownLatch finishedLatch = new CountDownLatch(1);
953+
CountDownLatch uninstalledLatch = new CountDownLatch(1);
954+
955+
kernel.getContext().addGlobalStateChangeListener((service, oldState, newState) -> {
956+
if (service.getName().equals("StateTransitionComponent")) {
957+
stateTransitions.add(newState);
958+
if (newState.equals(State.FINISHED)) {
959+
finishedLatch.countDown();
960+
} else if (newState.equals(State.UNINSTALLED)) {
961+
uninstalledLatch.countDown();
962+
}
963+
}
964+
});
965+
966+
kernel.launch();
967+
assertTrue(finishedLatch.await(30, TimeUnit.SECONDS), "component should reach FINISHED");
968+
969+
GenericExternalService component = (GenericExternalService) kernel.locate("StateTransitionComponent");
970+
component.requestUninstall();
971+
972+
assertTrue(uninstalledLatch.await(30, TimeUnit.SECONDS), "component should reach UNINSTALLED");
973+
974+
assertTrue(stateTransitions.contains(State.UNINSTALLING), "Should transition through UNINSTALLING");
975+
assertTrue(stateTransitions.contains(State.UNINSTALLED), "Should reach UNINSTALLED");
976+
}
977+
887978
private boolean isCgroupV2Supported() {
888979
return Files.exists(Paths.get("/sys/fs/cgroup/cgroup.controllers"));
889980
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"deploymentId": "RemoveAllComponents",
3+
"configurationArn": "Test",
4+
"components": {},
5+
"creationTimestamp": 1601276785086,
6+
"failureHandlingPolicy": "ROLLBACK",
7+
"componentUpdatePolicy": {
8+
"timeout": 60,
9+
"action": "NOTIFY_COMPONENTS"
10+
},
11+
"configurationValidationPolicy": {
12+
"timeout": 60
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"deploymentId": "DeployCustomTimeoutUninstallComponent",
3+
"configurationArn": "Test",
4+
"components": {
5+
"CustomTimeoutUninstallComponent": {
6+
"version": "1.0.0"
7+
}
8+
},
9+
"creationTimestamp": 1601276785085,
10+
"failureHandlingPolicy": "ROLLBACK",
11+
"componentUpdatePolicy": {
12+
"timeout": 60,
13+
"action": "NOTIFY_COMPONENTS"
14+
},
15+
"configurationValidationPolicy": {
16+
"timeout": 60
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"deploymentId": "DeployFailingUninstallComponent",
3+
"configurationArn": "Test",
4+
"components": {
5+
"FailingUninstallComponent": {
6+
"version": "1.0.0"
7+
}
8+
},
9+
"creationTimestamp": 1601276785085,
10+
"failureHandlingPolicy": "ROLLBACK",
11+
"componentUpdatePolicy": {
12+
"timeout": 60,
13+
"action": "NOTIFY_COMPONENTS"
14+
},
15+
"configurationValidationPolicy": {
16+
"timeout": 60
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"deploymentId": "DeployUninstallTestComponent",
3+
"configurationArn": "Test",
4+
"components": {
5+
"UninstallTestComponent": {
6+
"version": "1.0.0"
7+
}
8+
},
9+
"creationTimestamp": 1601276785085,
10+
"failureHandlingPolicy": "ROLLBACK",
11+
"componentUpdatePolicy": {
12+
"timeout": 60,
13+
"action": "NOTIFY_COMPONENTS"
14+
},
15+
"configurationValidationPolicy": {
16+
"timeout": 60
17+
}
18+
}

0 commit comments

Comments
 (0)