diff --git a/aca.camera/Aca.Camera.sln b/aca.camera/Aca.Camera.sln new file mode 100644 index 0000000..4280487 --- /dev/null +++ b/aca.camera/Aca.Camera.sln @@ -0,0 +1,45 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CameraService", "src\CameraService\CameraService.csproj", "{0DE5D578-9DCA-4E30-8ED0-3C412C7A48B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CameraControlService", "src\CameraControlService\CameraControlService.csproj", "{FF06E45F-865B-4420-8B75-DB70ED57ACAB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NotificationService", "src\NotificationService\NotificationService.csproj", "{AA6C912B-EFAF-43D3-A2A4-506F0A1821C1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DD83AB62-DD46-4797-8B90-A46BF1BA1AE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CameraSimulator", "src\CameraSimulator\CameraSimulator.csproj", "{6E1CDBDC-9CDE-4D2B-9D8D-4DC5C6E8688E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0DE5D578-9DCA-4E30-8ED0-3C412C7A48B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DE5D578-9DCA-4E30-8ED0-3C412C7A48B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DE5D578-9DCA-4E30-8ED0-3C412C7A48B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DE5D578-9DCA-4E30-8ED0-3C412C7A48B1}.Release|Any CPU.Build.0 = Release|Any CPU + {FF06E45F-865B-4420-8B75-DB70ED57ACAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF06E45F-865B-4420-8B75-DB70ED57ACAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF06E45F-865B-4420-8B75-DB70ED57ACAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF06E45F-865B-4420-8B75-DB70ED57ACAB}.Release|Any CPU.Build.0 = Release|Any CPU + {AA6C912B-EFAF-43D3-A2A4-506F0A1821C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA6C912B-EFAF-43D3-A2A4-506F0A1821C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA6C912B-EFAF-43D3-A2A4-506F0A1821C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA6C912B-EFAF-43D3-A2A4-506F0A1821C1}.Release|Any CPU.Build.0 = Release|Any CPU + {6E1CDBDC-9CDE-4D2B-9D8D-4DC5C6E8688E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E1CDBDC-9CDE-4D2B-9D8D-4DC5C6E8688E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E1CDBDC-9CDE-4D2B-9D8D-4DC5C6E8688E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E1CDBDC-9CDE-4D2B-9D8D-4DC5C6E8688E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6E1CDBDC-9CDE-4D2B-9D8D-4DC5C6E8688E} = {DD83AB62-DD46-4797-8B90-A46BF1BA1AE5} + EndGlobalSection +EndGlobal diff --git a/aca.camera/deploy/README.md b/aca.camera/deploy/README.md new file mode 100644 index 0000000..e549207 --- /dev/null +++ b/aca.camera/deploy/README.md @@ -0,0 +1,44 @@ +# Create the resource group + +``` shell +az group create -n aca-camera -l eastus +``` + +# Create the bicep deployment + +``` shell +az deployment group create -g aca-camera --template-file main.bicep +``` + +# Logs Analytics Query + +``` shell +ContainerAppConsoleLogs_CL +| where ContainerAppName_s == 'camera-control' +| project Log_State_Message_s, TimeGenerated +| order by TimeGenerated desc +``` + +# Log stream + +``` shell +az containerapp logs show -n camera-control -g aca-camera +``` + +# Connect to Console + +``` shell +az containerapp exec -n camera-control -g aca-camera --command bash +``` + +# Cleanup + +``` shell +az group delete -n aca-camera +``` + +## References: + +* [Azure Container Apps Virtual Network Integration](https://techcommunity.microsoft.com/t5/apps-on-azure-blog/azure-container-apps-virtual-network-integration/ba-p/3096932) +* [Quickstart: Deploy your first container app](https://docs.microsoft.com/en-us/azure/container-apps/get-started?ocid=AID3042118&tabs=bash) +* [Azure Container Apps GitHub](https://github.com/microsoft/azure-container-apps) \ No newline at end of file diff --git a/aca.camera/deploy/aca/aca_environment.bicep b/aca.camera/deploy/aca/aca_environment.bicep new file mode 100644 index 0000000..fa1df80 --- /dev/null +++ b/aca.camera/deploy/aca/aca_environment.bicep @@ -0,0 +1,158 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +@description('Specifies the VNET.') +param vnetId string + +@description('Storage Account Name') +param accountName string = 'daprcfmstorage' + +@description('Storage Account Key') +@secure() +param storageaccountkey string + +@description('EventHubs Connection String') +param evhConnectionString string + +@description('CosmosDB Name') +param cosmosDbName string + +resource logs 'Microsoft.OperationalInsights/workspaces@2020-03-01-preview' = { + name: 'container-apps-logs' + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource insights 'Microsoft.Insights/components@2020-02-02' = { + name: 'container-apps-insights' + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logs.id + } +} + +// https://github.com/Azure/azure-rest-api-specs/blob/cca8e03063c627f256fe0b3761db82450b25fdbb/specification/app/resource-manager/Microsoft.App/preview/2022-01-01-preview/ManagedEnvironments.json#L660 +resource app_environment 'Microsoft.App/managedEnvironments@2022-01-01-preview' = { + name: 'container-apps-env' + location: location + properties: { + daprAIInstrumentationKey: insights.properties.InstrumentationKey + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logs.properties.customerId + sharedKey: logs.listKeys().primarySharedKey + } + } + vnetConfiguration: { + internal: true + infrastructureSubnetId: '${vnetId}/subnets/apps' + } + } +} + +module aca_environment_dns './aca_environment_dns.bicep' = { + name: 'aca_environment_dns-module' + params: { + fqdn: app_environment.properties.defaultDomain + vnetId: vnetId + staticIP: app_environment.properties.staticIp + } +} + +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2021-01-15' existing = { + name: cosmosDbName +} + +resource dapr_state_store 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = { + name: 'statestore' + parent: app_environment + properties: { + componentType: 'state.azure.cosmosdb' + version: 'v1' + secrets: [ + { + name: 'masterkey' + value: cosmos.listKeys().primaryMasterKey + } + ] + metadata: [ + { + name: 'url' + value: cosmos.properties.documentEndpoint + } + { + name: 'database' + value: 'cameras' + } + { + name: 'collection' + value: 'state' + } + { + name: 'masterKey' + secretRef: 'masterkey' + } + { + name: 'actorStateStore' + value: 'true' + } + ] + scopes: [] + } +} + +resource dapr_pub_sub 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = { + name: 'pubsub' + parent: app_environment + properties: { + componentType: 'pubsub.azure.eventhubs' + version: 'v1' + secrets: [ + { + name: 'evhconnectionstring' + value: evhConnectionString + } + { + name: 'storageaccountkey' + value: storageaccountkey + } + ] + metadata: [ + { + name: 'connectionString' + secretRef: 'evhconnectionstring' + } + { + name: 'enableEntityManagement' + value: 'false' + } + { + name: 'storageAccountName' + value: accountName + } + { + name: 'storageContainerName' + value: 'subscribers' + } + { + name: 'storageAccountKey' + secretRef: 'storageaccountkey' + } + ] + scopes: [ + 'camera-control' + 'notification-service' + 'camera-simulator' + ] + } +} + +output id string = app_environment.id diff --git a/aca.camera/deploy/aca/aca_environment_dns.bicep b/aca.camera/deploy/aca/aca_environment_dns.bicep new file mode 100644 index 0000000..33e27e4 --- /dev/null +++ b/aca.camera/deploy/aca/aca_environment_dns.bicep @@ -0,0 +1,40 @@ +@description('Specifies the VNET.') +param vnetId string + +@description('Specifies the Azure Container App FQDN.') +param fqdn string + +@description('Specifies the Azure Container App Load Balancer IP.') +param staticIP string + +// Create the Private DNS Zone +resource aca_dns 'Microsoft.Network/privateDnsZones@2018-09-01' = { + name: fqdn + location: 'global' +} + +// Create A record pointing all subdomains to the Azure Container App Static IP +resource a_record 'Microsoft.Network/privateDnsZones/A@2020-06-01' = { + name: '*' + parent: aca_dns + properties: { + aRecords: [ + { + ipv4Address: staticIP + } + ] + ttl: 3600 + } +} + +// Link the Private DNS Zone with the VNET +resource vnet_dns_link 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2018-09-01' = { + name: '${aca_dns.name}/private-network' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: vnetId + } + } +} diff --git a/aca.camera/deploy/bastion/bastion.bicep b/aca.camera/deploy/bastion/bastion.bicep new file mode 100644 index 0000000..baf21ec --- /dev/null +++ b/aca.camera/deploy/bastion/bastion.bicep @@ -0,0 +1,39 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +@description('Specifies the VNET.') +param vnetId string + +@description('Specifies the Subnet.') +param subnetName string + +resource publicIpAddressForBastion 'Microsoft.Network/publicIpAddresses@2020-08-01' = { + name: 'aca-bastion-public-ip' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource bastionHost 'Microsoft.Network/bastionHosts@2021-03-01' = { + name: 'bastion' + location: location + properties: { + ipConfigurations: [ + { + name: 'IpConf' + properties: { + subnet: { + id: '${vnetId}/subnets/${subnetName}' + } + publicIPAddress: { + id: publicIpAddressForBastion.id + } + } + } + ] + } +} diff --git a/aca.camera/deploy/cosmosdb/cosmosdb.bicep b/aca.camera/deploy/cosmosdb/cosmosdb.bicep new file mode 100644 index 0000000..01a4b3d --- /dev/null +++ b/aca.camera/deploy/cosmosdb/cosmosdb.bicep @@ -0,0 +1,49 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2021-01-15' = { + name: 'dapr-aca-cosmosdb' + kind: 'GlobalDocumentDB' + location: location + properties: { + consistencyPolicy: { + defaultConsistencyLevel: 'Strong' + } + locations: [ + { + isZoneRedundant: false + locationName: location + } + ] + databaseAccountOfferType: 'Standard' + enableAutomaticFailover: false + } +} + +resource cosmos_databaseName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2021-01-15' = { + parent: cosmos + name: 'runners' + properties: { + resource: { + id: 'runners' + } + } +} + +resource cosmos_containerName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2021-01-15' = { + parent: cosmos_databaseName + name: 'state' + properties: { + resource: { + id: 'state' + partitionKey: { + paths: [ + '/partitionKey' + ] + kind: 'Hash' + } + } + } +} + +output name string = cosmos.name diff --git a/aca.camera/deploy/evh/evh.bicep b/aca.camera/deploy/evh/evh.bicep new file mode 100644 index 0000000..0606b04 --- /dev/null +++ b/aca.camera/deploy/evh/evh.bicep @@ -0,0 +1,45 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +resource evh_namespace 'Microsoft.EventHub/namespaces@2021-11-01' = { + name: 'daprevh' + location: location + sku: { + name: 'Standard' + } + properties: { + zoneRedundant: false + } +} + +resource evh 'Microsoft.EventHub/namespaces/eventhubs@2021-11-01' = { + name: 'race-control' + parent: evh_namespace + properties: { + partitionCount: 2 + messageRetentionInDays: 1 + } +} + +resource race_control_consumer 'Microsoft.EventHub/namespaces/eventhubs/consumergroups@2021-11-01' = { + name: 'race-control' + parent: evh +} + +resource check_point_consumer 'Microsoft.EventHub/namespaces/eventhubs/consumergroups@2021-11-01' = { + name: 'check-point' + parent: evh +} + +resource auth_evh 'Microsoft.EventHub/namespaces/eventhubs/authorizationRules@2021-11-01' = { + name: 'DaprListenSend' + parent: evh + properties: { + rights: [ + 'Listen' + 'Send' + ] + } +} + +output connectionString string = auth_evh.listKeys().primaryConnectionString diff --git a/aca.camera/deploy/main.bicep b/aca.camera/deploy/main.bicep new file mode 100644 index 0000000..037292b --- /dev/null +++ b/aca.camera/deploy/main.bicep @@ -0,0 +1,229 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +module vnet './vnet/vnet.bicep' = { + name: 'vnet-module' + params: { + location: location + } +} + +module bastion './bastion/bastion.bicep' = { + name: 'bastion-module' + params: { + location: location + vnetId: vnet.outputs.id + subnetName: 'AzureBastionSubnet' + } +} + +module vm './vm/vm.bicep' = { + name: 'vm-module' + params: { + location: location + vnetId: vnet.outputs.id + subnetName: 'jumpbox' + } +} + +module storage './storage/storage.bicep' = { + name: 'storage-module' + params: { + location: location + // vnetId: vnet.outputs.id + // subnetName: 'endpoints' + } +} + +module evh './evh/evh.bicep' = { + name: 'evh-module' + params: { + location: location + } +} + +module cosmos './cosmosdb/cosmosdb.bicep' = { + name: 'cosmosdb-module' + params: { + location: location + } +} + +module aca_environment './aca/aca_environment.bicep' = { + name: 'aca_environment-module' + params: { + location: location + vnetId: vnet.outputs.id + storageaccountkey: storage.outputs.key + evhConnectionString: evh.outputs.connectionString + cosmosDbName: cosmos.outputs.name + } +} + +resource camera_control 'Microsoft.App/containerApps@2022-01-01-preview' = { + name: 'camera-control' + location: location + identity: { + type: 'None' + } + properties: { + managedEnvironmentId: aca_environment.outputs.id + configuration: { + ingress: { + external: true + targetPort: 80 + } + secrets: [] + dapr: { + enabled: true + appId: 'camera-control' + appProtocol: 'http' + appPort: 80 + } + } + template: { + containers: [ + { + image: 'cmendibl3/aca-camera-control' + name: 'camera-control' + env: [] + resources: { + cpu: '0.5' + memory: '1Gi' + } + } + ] + scale: { + minReplicas: 1 + maxReplicas: 1 + } + } + } +} + +resource notification 'Microsoft.App/containerApps@2022-01-01-preview' = { + name: 'notification-service' + location: location + identity: { + type: 'None' + } + properties: { + managedEnvironmentId: aca_environment.outputs.id + configuration: { + ingress: { + external: true + targetPort: 80 + } + secrets: [] + dapr: { + enabled: true + appId: 'notification-service' + appProtocol: 'http' + appPort: 80 + } + } + template: { + containers: [ + { + image: 'cmendibl3/aca-camera-notification' + name: 'notification-service' + env: [] + resources: { + cpu: '0.5' + memory: '1Gi' + } + } + ] + scale: { + minReplicas: 1 + maxReplicas: 1 + } + } + } +} + +resource camera 'Microsoft.App/containerApps@2022-01-01-preview' = { + name: 'camera-service' + location: location + identity: { + type: 'None' + } + properties: { + managedEnvironmentId: aca_environment.outputs.id + configuration: { + ingress: { + external: true + targetPort: 80 + } + secrets: [] + dapr: { + enabled: true + appId: 'camera-service' + appProtocol: 'http' + appPort: 80 + } + } + template: { + containers: [ + { + image: 'cmendibl3/aca-camera-service' + name: 'camera-service' + env: [] + resources: { + cpu: '0.5' + memory: '1Gi' + } + } + ] + scale: { + minReplicas: 1 + maxReplicas: 5 + rules: [ + { + name: 'http-rule' + http: { + metadata: { + concurrentRequests: '5' + } + } + } + ] + } + } + } +} + +resource camera_simulator 'Microsoft.App/containerApps@2022-01-01-preview' = { + name: 'camera-simulator' + location: location + identity: { + type: 'None' + } + properties: { + managedEnvironmentId: aca_environment.outputs.id + configuration: { + secrets: [] + dapr: { + enabled: true + appId: 'camera-simulator' + } + } + template: { + containers: [ + { + image: 'cmendibl3/aca-camera-simulator' + name: 'camera-simulator' + env: [] + resources: { + cpu: '1' + memory: '2Gi' + } + } + ] + scale: { + minReplicas: 1 + maxReplicas: 1 + } + } + } +} diff --git a/aca.camera/deploy/storage/storage.bicep b/aca.camera/deploy/storage/storage.bicep new file mode 100644 index 0000000..83a1397 --- /dev/null +++ b/aca.camera/deploy/storage/storage.bicep @@ -0,0 +1,33 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +// @description('Specifies the VNET.') +// param vnetId string + +// @description('Specifies the Subnet.') +// param subnetName string + +resource storage 'Microsoft.Storage/storageAccounts@2021-08-01' = { + name: 'daprcfmstorage' + location: location + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } + properties: { + allowBlobPublicAccess: false + } +} + +resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = { + name: '${storage.name}/default/tweets' + properties: {} +} + +resource subscribers 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = { + name: '${storage.name}/default/subscribers' + properties: {} +} + +output key string = storage.listKeys().keys[0].value +output blobConnectionString string = 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${listKeys(storage.id, storage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' diff --git a/aca.camera/deploy/vm/vm.bicep b/aca.camera/deploy/vm/vm.bicep new file mode 100644 index 0000000..8c43bf5 --- /dev/null +++ b/aca.camera/deploy/vm/vm.bicep @@ -0,0 +1,68 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +@description('Specifies the VNET.') +param vnetId string + +@description('Specifies the Subnet.') +param subnetName string + +@description('Specifies the user name.') +param user_name string = 'azadmin' + +resource vm_nic 'Microsoft.Network/networkInterfaces@2020-08-01' = { + name: 'vm-nic' + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfiguration' + properties: { + privateIPAllocationMethod: 'Dynamic' + subnet: { + id: '${vnetId}/subnets/${subnetName}' + } + primary: true + privateIPAddressVersion: 'IPv4' + } + } + ] + } +} + +resource jumpbox 'Microsoft.Compute/virtualMachines@2021-03-01' = { + name: 'jumpbox' + location: location + properties: { + hardwareProfile: { + vmSize: 'Standard_D2s_v3' + } + storageProfile: { + osDisk: { + createOption: 'FromImage' + osType: 'Windows' + managedDisk: { + storageAccountType: 'StandardSSD_LRS' + } + } + imageReference: { + publisher: 'MicrosoftWindowsDesktop' + offer: 'Windows-10' + sku: '19H1-ent' + version: '18362.1198.2011031735' + } + } + osProfile: { + computerName: 'testcomputer' + adminUsername: user_name + adminPassword: 'Aca123456.' + } + networkProfile: { + networkInterfaces: [ + { + id: vm_nic.id + } + ] + } + } +} diff --git a/aca.camera/deploy/vnet/vnet.bicep b/aca.camera/deploy/vnet/vnet.bicep new file mode 100644 index 0000000..e817842 --- /dev/null +++ b/aca.camera/deploy/vnet/vnet.bicep @@ -0,0 +1,70 @@ +@description('Specifies the location for resources.') +param location string = resourceGroup().location + +// Create VNET +resource vnet 'Microsoft.Network/virtualNetworks@2020-11-01' = { + name: 'container-apps-vnet' + location: location + properties: { + addressSpace: { + addressPrefixes: [ + '10.0.0.0/8' + ] + } + subnets: [ + { + name: 'controlplane' + properties: { + addressPrefix: '10.240.0.0/16' + serviceEndpoints: [] + delegations: [] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + } + } + { + name: 'apps' + properties: { + addressPrefix: '10.241.0.0/16' + serviceEndpoints: [] + delegations: [] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + } + } + { + name: 'endpoints' + properties: { + addressPrefix: '10.242.0.0/16' + serviceEndpoints: [] + delegations: [] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } + } + { + name: 'jumpbox' + properties: { + addressPrefix: '10.243.0.0/16' + serviceEndpoints: [] + delegations: [] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + } + } + { + name: 'AzureBastionSubnet' + properties: { + addressPrefix: '10.24.0.0/27' + serviceEndpoints: [] + delegations: [] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + } + } + ] + enableDdosProtection: false + } +} + +output id string = vnet.id diff --git a/aca.camera/src/CameraControlService/Actors/CameraActor.cs b/aca.camera/src/CameraControlService/Actors/CameraActor.cs new file mode 100644 index 0000000..6fb43e5 --- /dev/null +++ b/aca.camera/src/CameraControlService/Actors/CameraActor.cs @@ -0,0 +1,41 @@ +namespace CameraControlService.Actors; + +public class CameraActor : Actor, ICameraActor, IRemindable +{ + private readonly DaprClient daprClient; + + public CameraActor(ActorHost host, DaprClient daprClient) : base(host) + { + this.daprClient = daprClient; + } + + public async Task RegisterMotionAsync(CameraMotionDetected msg) + { + try + { + await UnregisterReminderAsync("CameraOffline"); + + var cameraState = new CameraState(msg.CameraId, msg.Timestamp, msg.Timestamp); + await this.StateManager.SetStateAsync("CameraState", cameraState); + + await RegisterReminderAsync("CameraOffline", null, + TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(120)); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in RegisterMotion"); + } + } + + public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) + { + if (reminderName == "CameraOffline") + { + await UnregisterReminderAsync("CameraOffline"); + + var cameraState = await this.StateManager.GetStateAsync("CameraState"); + + Logger.LogInformation($"Camera with Id: {cameraState.CameraId} is offline"); + } + } +} \ No newline at end of file diff --git a/aca.camera/src/CameraControlService/Actors/ICameraActor.cs b/aca.camera/src/CameraControlService/Actors/ICameraActor.cs new file mode 100644 index 0000000..70f4ab8 --- /dev/null +++ b/aca.camera/src/CameraControlService/Actors/ICameraActor.cs @@ -0,0 +1,6 @@ +namespace CameraControlService.Actors; + +public interface ICameraActor : IActor +{ + public Task RegisterMotionAsync(CameraMotionDetected msg); +} diff --git a/aca.camera/src/CameraControlService/CameraControlService.csproj b/aca.camera/src/CameraControlService/CameraControlService.csproj new file mode 100644 index 0000000..6e84964 --- /dev/null +++ b/aca.camera/src/CameraControlService/CameraControlService.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/aca.camera/src/CameraControlService/Dockerfile b/aca.camera/src/CameraControlService/Dockerfile new file mode 100644 index 0000000..71cdcd8 --- /dev/null +++ b/aca.camera/src/CameraControlService/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build-env +WORKDIR /app + +# Copy necessary files and restore as distinct layer +COPY CameraControlService.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY . ./ +RUN dotnet publish -c Release -o out CameraControlService.csproj + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim +COPY --from=build-env /app/out . + +# Start +ENTRYPOINT ["dotnet", "CameraControlService.dll"] \ No newline at end of file diff --git a/aca.camera/src/CameraControlService/Events/CameraMotionDetected.cs b/aca.camera/src/CameraControlService/Events/CameraMotionDetected.cs new file mode 100644 index 0000000..368b9ae --- /dev/null +++ b/aca.camera/src/CameraControlService/Events/CameraMotionDetected.cs @@ -0,0 +1,3 @@ +namespace CameraControlService.Events; + +public record struct CameraMotionDetected(int CameraId, DateTime Timestamp); diff --git a/aca.camera/src/CameraControlService/GlobalUsings.cs b/aca.camera/src/CameraControlService/GlobalUsings.cs new file mode 100644 index 0000000..1151dc6 --- /dev/null +++ b/aca.camera/src/CameraControlService/GlobalUsings.cs @@ -0,0 +1,7 @@ +global using CameraControlService.Actors; +global using CameraControlService.Events; +global using CameraControlService.Models; +global using Dapr.Client; +global using Dapr.Actors; +global using Dapr.Actors.Runtime; +global using Dapr.Actors.Client; \ No newline at end of file diff --git a/aca.camera/src/CameraControlService/Models/CameraState.cs b/aca.camera/src/CameraControlService/Models/CameraState.cs new file mode 100644 index 0000000..42f40ef --- /dev/null +++ b/aca.camera/src/CameraControlService/Models/CameraState.cs @@ -0,0 +1,15 @@ +namespace CameraControlService.Models; + +public record struct CameraState +{ + public int CameraId { get; init; } + public DateTime? LastSeenAt { get; init; } + public DateTime? MotionDetectedAt { get; init; } + + public CameraState(int cameraId, DateTime? lastSeenAt, DateTime? motionDetectedAt = null) + { + this.CameraId = cameraId; + this.LastSeenAt = lastSeenAt; + this.MotionDetectedAt = motionDetectedAt; + } +} \ No newline at end of file diff --git a/aca.camera/src/CameraControlService/Program.cs b/aca.camera/src/CameraControlService/Program.cs new file mode 100644 index 0000000..523084b --- /dev/null +++ b/aca.camera/src/CameraControlService/Program.cs @@ -0,0 +1,44 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprClient(); +builder.Services.AddActors(options => +{ + options.Actors.RegisterActor(); +}); + +var app = builder.Build(); + +// configure web-app +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} + +app.UseCloudEvents(); + +// configure routing +app.MapActorsHandlers(); +app.UseRouting(); + +app.UseEndpoints(e => + { + e.MapPost("/camera-control", async (CameraMotionDetected msg, ILogger logger) => + { + try + { + logger.LogInformation($"Received change for camwera {msg.CameraId.ToString()}"); + var actorId = new ActorId(msg.CameraId.ToString()); + var proxy = ActorProxy.Create(actorId, nameof(CameraActor)); + await proxy.RegisterMotionAsync(msg); + return Results.Ok(); + } + catch + { + return Results.StatusCode(500); + } + }).WithTopic("pubsub", "camera-control"); + + e.MapSubscribeHandler(); + }); + +app.Run(); diff --git a/aca.camera/src/CameraControlService/Properties/launchSettings.json b/aca.camera/src/CameraControlService/Properties/launchSettings.json new file mode 100644 index 0000000..60e373b --- /dev/null +++ b/aca.camera/src/CameraControlService/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7089", + "sslPort": 44333 + } + }, + "profiles": { + "RaceControlService": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7228;http://localhost:5009", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aca.camera/src/CameraControlService/README.md b/aca.camera/src/CameraControlService/README.md new file mode 100644 index 0000000..fd56c8c --- /dev/null +++ b/aca.camera/src/CameraControlService/README.md @@ -0,0 +1,2 @@ +docker build -t cmendibl3/aca-camera-control:latest . +docker push cmendibl3/aca-camera-control:latest \ No newline at end of file diff --git a/aca.camera/src/CameraControlService/appsettings.Development.json b/aca.camera/src/CameraControlService/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/aca.camera/src/CameraControlService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aca.camera/src/CameraService/CameraService.csproj b/aca.camera/src/CameraService/CameraService.csproj new file mode 100644 index 0000000..4ddcf42 --- /dev/null +++ b/aca.camera/src/CameraService/CameraService.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/aca.camera/src/CameraService/Dockerfile b/aca.camera/src/CameraService/Dockerfile new file mode 100644 index 0000000..d342a04 --- /dev/null +++ b/aca.camera/src/CameraService/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build-env +WORKDIR /app + +# Copy necessary files and restore as distinct layer +COPY CameraService.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY . ./ +RUN dotnet publish -c Release -o out CameraService.csproj + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim +COPY --from=build-env /app/out . + +# Start +ENTRYPOINT ["dotnet", "CameraService.dll"] \ No newline at end of file diff --git a/aca.camera/src/CameraService/GlobalUsings.cs b/aca.camera/src/CameraService/GlobalUsings.cs new file mode 100644 index 0000000..77b2f19 --- /dev/null +++ b/aca.camera/src/CameraService/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using CameraService.Models; +global using Dapr.Client; +global using RandomNameGeneratorLibrary; \ No newline at end of file diff --git a/aca.camera/src/CameraService/Models/Camera.cs b/aca.camera/src/CameraService/Models/Camera.cs new file mode 100644 index 0000000..7784cf3 --- /dev/null +++ b/aca.camera/src/CameraService/Models/Camera.cs @@ -0,0 +1,3 @@ +namespace CameraService.Models; + +public record struct Camera(int CameraId, string Owner); diff --git a/aca.camera/src/CameraService/Program.cs b/aca.camera/src/CameraService/Program.cs new file mode 100644 index 0000000..b62a23d --- /dev/null +++ b/aca.camera/src/CameraService/Program.cs @@ -0,0 +1,36 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprClient(); + +// Create Dapr Client +var client = new DaprClientBuilder() + .Build(); + +var app = builder.Build(); + +app.UseCloudEvents(); + +app.UseRouting(); + +var rnd = new Random(); +var nameGenerator = new PersonNameGenerator(rnd); + +var inMemoryCameras = new Dictionary(); + +app.MapGet("/{cameraId:int}", (int cameraId) => + { + Camera camera = default; + if (!inMemoryCameras.Keys.Contains(cameraId)) + { + var owner = nameGenerator.GenerateRandomFirstAndLastName(); + camera = new Camera(cameraId, owner); + } + else + { + camera = inMemoryCameras[cameraId]; + } + + return camera; + }); + +app.Run(); diff --git a/aca.camera/src/CameraService/Properties/launchSettings.json b/aca.camera/src/CameraService/Properties/launchSettings.json new file mode 100644 index 0000000..7f5fc14 --- /dev/null +++ b/aca.camera/src/CameraService/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55097", + "sslPort": 44382 + } + }, + "profiles": { + "RunnerService": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7176;http://localhost:5211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aca.camera/src/CameraService/README.md b/aca.camera/src/CameraService/README.md new file mode 100644 index 0000000..bdee59d --- /dev/null +++ b/aca.camera/src/CameraService/README.md @@ -0,0 +1,2 @@ +docker build -t cmendibl3/aca-camera-service:latest . +docker push cmendibl3/aca-camera-service:latest \ No newline at end of file diff --git a/aca.camera/src/CameraService/appsettings.Development.json b/aca.camera/src/CameraService/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/aca.camera/src/CameraService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aca.camera/src/CameraSimulator/Camera.cs b/aca.camera/src/CameraSimulator/Camera.cs new file mode 100644 index 0000000..4cfc67a --- /dev/null +++ b/aca.camera/src/CameraSimulator/Camera.cs @@ -0,0 +1,60 @@ +public class Camera +{ + DaprClient daprClient; + private Random rnd; + private int cameraId; + private int minStartDelayInMS = 50; + private int maxStartDelayInMS = 100; + private int minHalfDelayInS = 30; + private int maxHalfDelayInS = 60; + + public Camera(int cameraId, DaprClient daprClient) + { + rnd = new Random(); + this.cameraId = cameraId; + this.daprClient = daprClient; + } + + public Task Start() + { + while (true) + { + Console.WriteLine($"Starting simulation for camera: {cameraId}"); + + try + { + var startDelay = TimeSpan.FromMilliseconds(rnd.Next(minStartDelayInMS, maxStartDelayInMS) + rnd.NextDouble()); + Task.Delay(startDelay).Wait(); + + Task.Run(async () => + { + Console.WriteLine($"Motione detected on Camera: {cameraId}"); + var cameraMotionDetected = new CameraMotionDetected + { + CameraId = cameraId, + Timestamp = DateTime.Now + }; + await daprClient.PublishEventAsync("pubsub", "camera-control", cameraMotionDetected); + + var halfDelay = TimeSpan.FromSeconds(rnd.Next(minHalfDelayInS, maxHalfDelayInS) + rnd.NextDouble()); + Task.Delay(halfDelay).Wait(); + Console.WriteLine($"Motione detected on Camera: {cameraId}"); + cameraMotionDetected = cameraMotionDetected with { Timestamp = DateTime.Now }; + await daprClient.PublishEventAsync("pubsub", "camera-control", cameraMotionDetected); + + var finishDelay = TimeSpan.FromSeconds(rnd.Next(minHalfDelayInS, maxHalfDelayInS) + rnd.NextDouble()); + Task.Delay(finishDelay).Wait(); + Console.WriteLine($"Motione detected on Camera: {cameraId}"); + cameraMotionDetected = cameraMotionDetected with { Timestamp = DateTime.Now }; + await daprClient.PublishEventAsync("pubsub", "camera-control", cameraMotionDetected); + + }).Wait(); + } + catch (Exception ex) + { + Console.WriteLine($"Camera {cameraId} exception: {ex.Message}"); + } + Console.WriteLine($"Finished simulation for camera: {cameraId}."); + } + } +} \ No newline at end of file diff --git a/aca.camera/src/CameraSimulator/CameraSimulator.csproj b/aca.camera/src/CameraSimulator/CameraSimulator.csproj new file mode 100644 index 0000000..489a084 --- /dev/null +++ b/aca.camera/src/CameraSimulator/CameraSimulator.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/aca.camera/src/CameraSimulator/Dockerfile b/aca.camera/src/CameraSimulator/Dockerfile new file mode 100644 index 0000000..fbaaf6e --- /dev/null +++ b/aca.camera/src/CameraSimulator/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build-env +WORKDIR /app + +# Copy necessary files and restore as distinct layer +COPY CameraSimulator.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY . ./ +RUN dotnet publish -c Release -o out CameraSimulator.csproj + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim +COPY --from=build-env /app/out . + +# Start +ENTRYPOINT ["dotnet", "CameraSimulator.dll"] \ No newline at end of file diff --git a/aca.camera/src/CameraSimulator/Events/CameraMotionDetected.cs b/aca.camera/src/CameraSimulator/Events/CameraMotionDetected.cs new file mode 100644 index 0000000..30a792d --- /dev/null +++ b/aca.camera/src/CameraSimulator/Events/CameraMotionDetected.cs @@ -0,0 +1,3 @@ +namespace CameraSimulator.Events; + +public record struct CameraMotionDetected(int CameraId, DateTime Timestamp); diff --git a/aca.camera/src/CameraSimulator/GlobalUsings.cs b/aca.camera/src/CameraSimulator/GlobalUsings.cs new file mode 100644 index 0000000..633c420 --- /dev/null +++ b/aca.camera/src/CameraSimulator/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using CameraSimulator.Events; +global using Dapr.Client; diff --git a/aca.camera/src/CameraSimulator/Program.cs b/aca.camera/src/CameraSimulator/Program.cs new file mode 100644 index 0000000..b26e42b --- /dev/null +++ b/aca.camera/src/CameraSimulator/Program.cs @@ -0,0 +1,11 @@ +var daprClient = new DaprClientBuilder().Build(); +var camerasCount = 1000; +var cameras = new Camera[camerasCount]; +for (var i = 0; i < camerasCount; i++) +{ + var id = i + 1; + cameras[i] = new Camera(id, daprClient); +} +Parallel.ForEach(cameras, camera => camera.Start()); + +Task.Run(() => Thread.Sleep(Timeout.Infinite)).Wait(); \ No newline at end of file diff --git a/aca.camera/src/CameraSimulator/README.md b/aca.camera/src/CameraSimulator/README.md new file mode 100644 index 0000000..736499b --- /dev/null +++ b/aca.camera/src/CameraSimulator/README.md @@ -0,0 +1,2 @@ +docker build -t cmendibl3/aca-camera-simulator:latest . +docker push cmendibl3/aca-camera-simulator:latest \ No newline at end of file diff --git a/aca.camera/src/NotificationService/Dockerfile b/aca.camera/src/NotificationService/Dockerfile new file mode 100644 index 0000000..4f6a6eb --- /dev/null +++ b/aca.camera/src/NotificationService/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build-env +WORKDIR /app + +# Copy necessary files and restore as distinct layer +COPY NotificationService.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY . ./ +RUN dotnet publish -c Release -o out NotificationService.csproj + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim +COPY --from=build-env /app/out . + +# Start +ENTRYPOINT ["dotnet", "NotificationService.dll"] \ No newline at end of file diff --git a/aca.camera/src/NotificationService/Events/CameraRegistered.cs b/aca.camera/src/NotificationService/Events/CameraRegistered.cs new file mode 100644 index 0000000..6f72815 --- /dev/null +++ b/aca.camera/src/NotificationService/Events/CameraRegistered.cs @@ -0,0 +1,3 @@ +namespace NotificationService.Events; + +public record struct CameraMotionDetected(int CameraId, DateTime Timestamp); \ No newline at end of file diff --git a/aca.camera/src/NotificationService/GlobalUsings.cs b/aca.camera/src/NotificationService/GlobalUsings.cs new file mode 100644 index 0000000..0b037c6 --- /dev/null +++ b/aca.camera/src/NotificationService/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using NotificationService.Events; +global using CameraService.Models; +global using Dapr.Client; diff --git a/aca.camera/src/NotificationService/Models/Camera.cs b/aca.camera/src/NotificationService/Models/Camera.cs new file mode 100644 index 0000000..7784cf3 --- /dev/null +++ b/aca.camera/src/NotificationService/Models/Camera.cs @@ -0,0 +1,3 @@ +namespace CameraService.Models; + +public record struct Camera(int CameraId, string Owner); diff --git a/aca.camera/src/NotificationService/NotificationService.csproj b/aca.camera/src/NotificationService/NotificationService.csproj new file mode 100644 index 0000000..18b33a9 --- /dev/null +++ b/aca.camera/src/NotificationService/NotificationService.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/aca.camera/src/NotificationService/Program.cs b/aca.camera/src/NotificationService/Program.cs new file mode 100644 index 0000000..c02b6f9 --- /dev/null +++ b/aca.camera/src/NotificationService/Program.cs @@ -0,0 +1,26 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprClient(); + +var app = builder.Build(); + +app.UseCloudEvents(); + +app.UseRouting(); + +app.UseEndpoints(e => + { + e.MapPost("/camera-control", async (CameraMotionDetected msg, DaprClient daprClient, ILogger log) => + { + var result = daprClient.CreateInvokeMethodRequest(HttpMethod.Get, "camera-service", $"/{msg.CameraId}"); + var camera = await daprClient.InvokeMethodAsync(result); + + log.LogInformation($"Motion Detected!!! Notification should be sent to owner {camera.Owner} with Camera Id: {msg.CameraId}!!!"); + + return Results.Ok(); + }).WithTopic("pubsub", "camera-control"); + + e.MapSubscribeHandler(); + }); + +app.Run(); diff --git a/aca.camera/src/NotificationService/Properties/launchSettings.json b/aca.camera/src/NotificationService/Properties/launchSettings.json new file mode 100644 index 0000000..fbf5349 --- /dev/null +++ b/aca.camera/src/NotificationService/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:45653", + "sslPort": 44388 + } + }, + "profiles": { + "RaceCheckPointService": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7084;http://localhost:5111", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aca.camera/src/NotificationService/README.md b/aca.camera/src/NotificationService/README.md new file mode 100644 index 0000000..0bd966e --- /dev/null +++ b/aca.camera/src/NotificationService/README.md @@ -0,0 +1,2 @@ +docker build -t cmendibl3/aca-camera-notification:latest . +docker push cmendibl3/aca-camera-notification:latest \ No newline at end of file diff --git a/aca.camera/src/NotificationService/appsettings.Development.json b/aca.camera/src/NotificationService/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/aca.camera/src/NotificationService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/function_vnet_integration/src/package-lock.json b/function_vnet_integration/src/package-lock.json index 77fe354..4b9d7af 100644 --- a/function_vnet_integration/src/package-lock.json +++ b/function_vnet_integration/src/package-lock.json @@ -2108,9 +2108,9 @@ } }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": { "json5": "lib/cli.js" }, @@ -4200,9 +4200,9 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, "kareem": { "version": "2.4.1",