99import com .aws .greengrass .config .Topics ;
1010import com .aws .greengrass .dependency .State ;
1111import com .aws .greengrass .deployment .DeploymentConfigMerger ;
12+ import com .aws .greengrass .deployment .DeviceConfiguration ;
1213import com .aws .greengrass .deployment .model .ComponentUpdatePolicy ;
1314import com .aws .greengrass .deployment .model .Deployment ;
1415import com .aws .greengrass .deployment .model .DeploymentDocument ;
2526import com .aws .greengrass .logging .api .Logger ;
2627import com .aws .greengrass .logging .impl .GreengrassLogMessage ;
2728import com .aws .greengrass .logging .impl .LogManager ;
29+ import com .aws .greengrass .mqttclient .MqttClient ;
30+ import com .aws .greengrass .mqttclient .PublishRequest ;
2831import com .aws .greengrass .status .FleetStatusService ;
2932import com .aws .greengrass .testcommons .testutilities .GGExtension ;
3033import com .aws .greengrass .testcommons .testutilities .NoOpPathOwnershipHandler ;
3134import com .aws .greengrass .testcommons .testutilities .TestUtils ;
35+ import com .aws .greengrass .util .Coerce ;
36+ import com .aws .greengrass .util .GreengrassServiceClientFactory ;
37+ import com .aws .greengrass .util .Pair ;
3238import org .apache .commons .lang3 .SystemUtils ;
3339import org .hamcrest .Matchers ;
3440import org .junit .jupiter .api .AfterAll ;
3743import org .junit .jupiter .api .BeforeEach ;
3844import org .junit .jupiter .api .Test ;
3945import org .junit .jupiter .api .extension .ExtendWith ;
46+ import org .junit .jupiter .api .extension .ExtensionContext ;
47+ import org .mockito .Mock ;
4048import org .slf4j .event .Level ;
4149import software .amazon .awssdk .aws .greengrass .GreengrassCoreIPCClient ;
4250import software .amazon .awssdk .aws .greengrass .model .ComponentUpdatePolicyEvents ;
4351import software .amazon .awssdk .aws .greengrass .model .DeferComponentUpdateRequest ;
4452import software .amazon .awssdk .aws .greengrass .model .SubscribeToComponentUpdatesRequest ;
4553import software .amazon .awssdk .aws .greengrass .model .SubscribeToComponentUpdatesResponse ;
54+ import software .amazon .awssdk .awscore .exception .AwsServiceException ;
4655import software .amazon .awssdk .crt .io .SocketOptions ;
56+ import software .amazon .awssdk .crt .mqtt .MqttClientConnection ;
57+ import software .amazon .awssdk .crt .mqtt .MqttException ;
4758import software .amazon .awssdk .eventstreamrpc .EventStreamRPCConnection ;
4859import software .amazon .awssdk .eventstreamrpc .StreamResponseHandler ;
60+ import software .amazon .awssdk .http .SdkHttpClient ;
61+ import software .amazon .awssdk .iot .AwsIotMqttConnectionBuilder ;
4962import software .amazon .awssdk .services .greengrassv2 .model .DeploymentConfigurationValidationPolicy ;
63+ import software .amazon .awssdk .services .greengrassv2data .GreengrassV2DataClient ;
64+ import software .amazon .awssdk .services .greengrassv2data .model .GreengrassV2DataException ;
65+ import software .amazon .awssdk .services .greengrassv2data .model .ListThingGroupsForCoreDeviceRequest ;
5066
5167import java .io .IOException ;
5268import java .time .Duration ;
7692import static com .aws .greengrass .lifecyclemanager .GreengrassService .SERVICE_DEPENDENCIES_NAMESPACE_TOPIC ;
7793import static com .aws .greengrass .lifecyclemanager .GreengrassService .SERVICE_LIFECYCLE_NAMESPACE_TOPIC ;
7894import static com .aws .greengrass .lifecyclemanager .GreengrassService .SETENV_CONFIG_NAMESPACE ;
95+ import static com .aws .greengrass .testcommons .testutilities .ExceptionLogProtector .ignoreExceptionOfType ;
7996import static com .aws .greengrass .testcommons .testutilities .SudoUtil .assumeCanSudoShell ;
8097import static com .aws .greengrass .testcommons .testutilities .TestUtils .createCloseableLogListener ;
8198import static com .aws .greengrass .testcommons .testutilities .TestUtils .createServiceStateChangeWaiter ;
92109import static org .junit .jupiter .api .Assertions .assertThrows ;
93110import static org .junit .jupiter .api .Assertions .assertTrue ;
94111import static org .junit .jupiter .api .Assertions .fail ;
112+ import static org .mockito .ArgumentMatchers .any ;
113+ import static org .mockito .ArgumentMatchers .eq ;
114+ import static org .mockito .Mockito .when ;
95115import static software .amazon .awssdk .services .greengrassv2 .model .DeploymentComponentUpdatePolicyAction .NOTIFY_COMPONENTS ;
96116import static software .amazon .awssdk .services .greengrassv2 .model .DeploymentComponentUpdatePolicyAction .SKIP_NOTIFY_COMPONENTS ;
97117
98118@ ExtendWith (GGExtension .class )
99119class DeploymentConfigMergingTest extends BaseITCase {
120+ @ Mock
121+ private MqttClient mqttClient ;
122+ @ Mock
123+ private AwsIotMqttConnectionBuilder awsIotMqttConnectionBuilder ;
124+ @ Mock
125+ private MqttClientConnection mqttClientConnection ;
126+ @ Mock
127+ private GreengrassServiceClientFactory gscFactory ;
128+ @ Mock
129+ private GreengrassV2DataClient greengrassV2DataClient ;
130+
100131 private Kernel kernel ;
101132 private DeploymentConfigMerger deploymentConfigMerger ;
102133 private static SocketOptions socketOptions ;
@@ -111,6 +142,8 @@ static void initialize() {
111142 void before () {
112143 kernel = new Kernel ();
113144 NoOpPathOwnershipHandler .register (kernel );
145+ kernel .getContext ().put (MqttClient .class , mqttClient );
146+ kernel .getContext ().put (GreengrassServiceClientFactory .class , gscFactory );
114147 deploymentConfigMerger = kernel .getContext ().get (DeploymentConfigMerger .class );
115148 }
116149
@@ -371,14 +404,14 @@ void GIVEN_kernel_running_single_service_WHEN_merge_same_doc_happens_twice_THEN_
371404 put (SERVICES_NAMESPACE_TOPIC , new HashMap <String , Object >() {{
372405 put ("main" , new HashMap <String , Object >() {{
373406 put (SERVICE_DEPENDENCIES_NAMESPACE_TOPIC ,
374- Arrays .asList ("new_service" , DEFAULT_NUCLEUS_COMPONENT_NAME ));
407+ new ArrayList <>( Arrays .asList ("new_service" , DEFAULT_NUCLEUS_COMPONENT_NAME ) ));
375408 }});
376409
377410 put ("new_service" , new HashMap <String , Object >() {{
378411 put (SERVICE_LIFECYCLE_NAMESPACE_TOPIC , new HashMap <String , Object >() {{
379412 put (LIFECYCLE_RUN_NAMESPACE_TOPIC , "echo done" );
380413 }});
381- put (SERVICE_DEPENDENCIES_NAMESPACE_TOPIC , Arrays .asList ("new_service2" ));
414+ put (SERVICE_DEPENDENCIES_NAMESPACE_TOPIC , new ArrayList <>( Arrays .asList ("new_service2" ) ));
382415 }});
383416
384417 put ("new_service2" , new HashMap <String , Object >() {{
@@ -709,6 +742,144 @@ void GIVEN_kernel_running_service_WHEN_run_with_change_THEN_service_restarts() t
709742 }
710743 }
711744 }
745+
746+ @ Test
747+ void GIVEN_kernel_running_with_some_config_WHEN_connectivity_validation_successful_THEN_config_is_updated ()
748+ throws Throwable {
749+ // GIVEN
750+ DeviceConfiguration deviceConfiguration = new DeviceConfiguration (kernel .getConfig (), kernel .getKernelCommandLine (), "ThingName" , "xxxxxx-ats.iot.us-east-1.amazonaws.com" ,
751+ "xxxxxx.credentials.iot.us-east-1.amazonaws.com" , "privKeyFilePath" , "certFilePath" , "caFilePath" ,
752+ "us-east-1" , "roleAliasName" );
753+ kernel .getContext ().put (DeviceConfiguration .class , deviceConfiguration );
754+ ConfigPlatformResolver .initKernelWithMultiPlatformConfig (kernel , getClass ().getResource ("config.yaml" ));
755+
756+ // Mock Fss trigger at kernel launch
757+ when (mqttClient .publish (any (PublishRequest .class ))).thenReturn (CompletableFuture .completedFuture (0 ));
758+ kernel .launch ();
759+
760+ // Read deployment config from yaml file
761+ Map <String , Object > deploymentConfig = ConfigPlatformResolver
762+ .resolvePlatformMap (getClass ().getResource ("connectivityValidationConfig.yaml" ));
763+ int expectedMqttPort = 8080 ; // mqtt.port
764+ int expectedDataPlanePort = 443 ; // greengrassDataPlanePort
765+
766+ // Mock Successful Mqtt Validation
767+ when (mqttClient .createMqttConnectionBuilder (any (), any (), eq (null ))).thenReturn (awsIotMqttConnectionBuilder );
768+ when (awsIotMqttConnectionBuilder .withClientId (any ())).thenReturn (awsIotMqttConnectionBuilder );
769+ when (awsIotMqttConnectionBuilder .build ()).thenReturn (mqttClientConnection );
770+ CompletableFuture <Boolean > mqttConnection = CompletableFuture .completedFuture (true );
771+ when (mqttClientConnection .connect ()).thenReturn (mqttConnection );
772+
773+ // Mock Successful Http Validation
774+ Pair <SdkHttpClient , GreengrassV2DataClient > pair = new Pair <>(null , greengrassV2DataClient );
775+ when (gscFactory .createClientFromConfig (any ())).thenReturn (pair );
776+ when (greengrassV2DataClient .listThingGroupsForCoreDevice ((ListThingGroupsForCoreDeviceRequest ) any ()))
777+ .thenReturn (null );
778+
779+ // WHEN
780+ Topics t = kernel .findServiceTopic (FleetStatusService .FLEET_STATUS_SERVICE_TOPICS );
781+ assertNotNull (t , "FSS Topics should not be null before merging" );
782+ deploymentConfigMerger .mergeInNewConfig (testDeployment (), deploymentConfig ).get (60 , TimeUnit .SECONDS );
783+ t = kernel .findServiceTopic (FleetStatusService .FLEET_STATUS_SERVICE_TOPICS );
784+ assertNotNull (t , "FSS Topics should not be null after merging" );
785+
786+ // THEN
787+ deviceConfiguration = kernel .getContext ().get (DeviceConfiguration .class );
788+ int actualMqttPort = Coerce .toInt (deviceConfiguration .getMQTTNamespace ().find (MqttClient .MQTT_PORT_KEY ));
789+ assertEquals (expectedMqttPort , actualMqttPort );
790+ int actualDataPlanePort = Coerce .toInt (deviceConfiguration .getGreengrassDataPlanePort ());
791+ assertEquals (expectedDataPlanePort , actualDataPlanePort );
792+ }
793+
794+ @ Test
795+ void GIVEN_kernel_running_with_some_config_WHEN_mqtt_validation_fails_THEN_config_is_not_updated (ExtensionContext context )
796+ throws Throwable {
797+ ignoreExceptionOfType (context , MqttException .class );
798+ // GIVEN
799+ DeviceConfiguration deviceConfiguration = new DeviceConfiguration (kernel .getConfig (), kernel .getKernelCommandLine (), "ThingName" , "xxxxxx-ats.iot.us-east-1.amazonaws.com" ,
800+ "xxxxxx.credentials.iot.us-east-1.amazonaws.com" , "privKeyFilePath" , "certFilePath" , "caFilePath" ,
801+ "us-east-1" , "roleAliasName" );
802+ kernel .getContext ().put (DeviceConfiguration .class , deviceConfiguration );
803+ ConfigPlatformResolver .initKernelWithMultiPlatformConfig (kernel , getClass ().getResource ("config.yaml" ));
804+
805+ // Mock fss trigger at kernel launch
806+ when (mqttClient .publish (any (PublishRequest .class ))).thenReturn (CompletableFuture .completedFuture (0 ));
807+ kernel .launch ();
808+
809+ // Read deployment config from yaml file
810+ Map <String , Object > deploymentConfig = ConfigPlatformResolver
811+ .resolvePlatformMap (getClass ().getResource ("connectivityValidationConfig.yaml" ));
812+
813+ // Mock Failed Mqtt Validation
814+ when (mqttClient .createMqttConnectionBuilder (any (), any (), eq (null ))).thenReturn (awsIotMqttConnectionBuilder );
815+ when (awsIotMqttConnectionBuilder .withClientId (any ())).thenReturn (awsIotMqttConnectionBuilder );
816+ when (awsIotMqttConnectionBuilder .build ()).thenReturn (mqttClientConnection );
817+ when (mqttClientConnection .connect ()).thenThrow (new MqttException ("mocked failure" ));
818+
819+ // WHEN
820+ Topics t = kernel .findServiceTopic (FleetStatusService .FLEET_STATUS_SERVICE_TOPICS );
821+ assertNotNull (t , "FSS Topics should not be null before merging" );
822+ deploymentConfigMerger .mergeInNewConfig (testDeployment (), deploymentConfig ).get (60 , TimeUnit .SECONDS );
823+ t = kernel .findServiceTopic (FleetStatusService .FLEET_STATUS_SERVICE_TOPICS );
824+ assertNotNull (t , "FSS Topics should not be null after merging" );
825+
826+ // THEN
827+ deviceConfiguration = kernel .getContext ().get (DeviceConfiguration .class );
828+ int actualMqttPort = Coerce .toInt (deviceConfiguration .getMQTTNamespace ().find (MqttClient .MQTT_PORT_KEY ));
829+ assertEquals (0 , actualMqttPort );
830+ int actualDataPlanePort = Coerce .toInt (deviceConfiguration .getGreengrassDataPlanePort ());
831+ assertEquals (8443 , actualDataPlanePort );
832+ }
833+
834+ @ Test
835+ void GIVEN_kernel_running_with_some_config_WHEN_http_validation_fails_THEN_config_is_not_updated (ExtensionContext context )
836+ throws Throwable {
837+ ignoreExceptionOfType (context , GreengrassV2DataException .class );
838+ // GIVEN
839+ DeviceConfiguration deviceConfiguration = new DeviceConfiguration (kernel .getConfig (), kernel .getKernelCommandLine (), "ThingName" , "xxxxxx-ats.iot.us-east-1.amazonaws.com" ,
840+ "xxxxxx.credentials.iot.us-east-1.amazonaws.com" , "privKeyFilePath" , "certFilePath" , "caFilePath" ,
841+ "us-east-1" , "roleAliasName" );
842+ kernel .getContext ().put (DeviceConfiguration .class , deviceConfiguration );
843+ ConfigPlatformResolver .initKernelWithMultiPlatformConfig (kernel , getClass ().getResource ("config.yaml" ));
844+
845+ // Mock fss trigger at kernel launch
846+ when (mqttClient .publish (any (PublishRequest .class ))).thenReturn (CompletableFuture .completedFuture (0 ));
847+ kernel .launch ();
848+
849+ // Read deployment config from yaml file
850+ Map <String , Object > deploymentConfig = ConfigPlatformResolver
851+ .resolvePlatformMap (getClass ().getResource ("connectivityValidationConfig.yaml" ));
852+
853+ // Mock Successful Mqtt Validation
854+ when (mqttClient .createMqttConnectionBuilder (any (), any (), eq (null ))).thenReturn (awsIotMqttConnectionBuilder );
855+ when (awsIotMqttConnectionBuilder .withClientId (any ())).thenReturn (awsIotMqttConnectionBuilder );
856+ when (awsIotMqttConnectionBuilder .build ()).thenReturn (mqttClientConnection );
857+ CompletableFuture <Boolean > mqttConnection = new CompletableFuture <>();
858+ mqttConnection .complete (true );
859+ when (mqttClientConnection .connect ()).thenReturn (mqttConnection );
860+
861+ // Mock Failed Http Validation
862+ Pair <SdkHttpClient , GreengrassV2DataClient > pair = new Pair <>(null , greengrassV2DataClient );
863+ when (gscFactory .createClientFromConfig (any ())).thenReturn (pair );
864+ AwsServiceException exception = GreengrassV2DataException .builder ().message ("mocked failure" ).build ();
865+ when (greengrassV2DataClient .listThingGroupsForCoreDevice ((ListThingGroupsForCoreDeviceRequest ) any ()))
866+ .thenThrow (exception );
867+
868+ // WHEN
869+ Topics t = kernel .findServiceTopic (FleetStatusService .FLEET_STATUS_SERVICE_TOPICS );
870+ assertNotNull (t , "FSS Topics should not be null before merging" );
871+ deploymentConfigMerger .mergeInNewConfig (testDeployment (), deploymentConfig ).get (60 , TimeUnit .SECONDS );
872+ t = kernel .findServiceTopic (FleetStatusService .FLEET_STATUS_SERVICE_TOPICS );
873+ assertNotNull (t , "FSS Topics should not be null after merging" );
874+
875+ // THEN
876+ deviceConfiguration = kernel .getContext ().get (DeviceConfiguration .class );
877+ int actualMqttPort = Coerce .toInt (deviceConfiguration .getMQTTNamespace ().find (MqttClient .MQTT_PORT_KEY ));
878+ assertEquals (0 , actualMqttPort );
879+ int actualDataPlanePort = Coerce .toInt (deviceConfiguration .getGreengrassDataPlanePort ());
880+ assertEquals (8443 , actualDataPlanePort );
881+ }
882+
712883 private Deployment testDeployment () {
713884 DeploymentDocument doc = DeploymentDocument .builder ().timestamp (System .currentTimeMillis ()).deploymentId ("id" )
714885 .failureHandlingPolicy (FailureHandlingPolicy .DO_NOTHING )
0 commit comments