@alwaysai/device-agent 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/lib/application-control/environment-variables.d.ts +1 -0
  2. package/lib/application-control/environment-variables.d.ts.map +1 -1
  3. package/lib/application-control/environment-variables.js +22 -20
  4. package/lib/application-control/environment-variables.js.map +1 -1
  5. package/lib/application-control/environment-variables.test.js +37 -2
  6. package/lib/application-control/environment-variables.test.js.map +1 -1
  7. package/lib/application-control/install.js +1 -1
  8. package/lib/application-control/install.js.map +1 -1
  9. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +2 -2
  10. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  11. package/lib/cloud-connection/device-agent-cloud-connection.js +116 -99
  12. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  13. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  14. package/lib/cloud-connection/live-updates-handler.js +30 -25
  15. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  16. package/lib/cloud-connection/live-updates-handler.test.js +15 -0
  17. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  18. package/lib/cloud-connection/messages.d.ts +1 -3
  19. package/lib/cloud-connection/messages.d.ts.map +1 -1
  20. package/lib/cloud-connection/messages.js +1 -9
  21. package/lib/cloud-connection/messages.js.map +1 -1
  22. package/lib/cloud-connection/publisher.d.ts +1 -0
  23. package/lib/cloud-connection/publisher.d.ts.map +1 -1
  24. package/lib/cloud-connection/publisher.js +3 -0
  25. package/lib/cloud-connection/publisher.js.map +1 -1
  26. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  27. package/lib/cloud-connection/shadow-handler.js +10 -3
  28. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  29. package/lib/cloud-connection/shadow-handler.test.js +79 -28
  30. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  31. package/lib/cloud-connection/transaction-manager.d.ts +26 -6
  32. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  33. package/lib/cloud-connection/transaction-manager.js +103 -22
  34. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  35. package/lib/cloud-connection/transaction-manager.test.js +179 -13
  36. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  37. package/lib/subcommands/app/analytics.d.ts +10 -0
  38. package/lib/subcommands/app/analytics.d.ts.map +1 -0
  39. package/lib/subcommands/app/analytics.js +83 -0
  40. package/lib/subcommands/app/analytics.js.map +1 -0
  41. package/lib/subcommands/app/index.d.ts.map +1 -1
  42. package/lib/subcommands/app/index.js +3 -1
  43. package/lib/subcommands/app/index.js.map +1 -1
  44. package/lib/subcommands/app/models.d.ts +0 -5
  45. package/lib/subcommands/app/models.d.ts.map +1 -1
  46. package/lib/subcommands/app/models.js +11 -47
  47. package/lib/subcommands/app/models.js.map +1 -1
  48. package/lib/subcommands/app/status.d.ts +1 -0
  49. package/lib/subcommands/app/status.d.ts.map +1 -1
  50. package/lib/subcommands/app/status.js +14 -3
  51. package/lib/subcommands/app/status.js.map +1 -1
  52. package/lib/subcommands/app/version.d.ts +2 -1
  53. package/lib/subcommands/app/version.d.ts.map +1 -1
  54. package/lib/subcommands/app/version.js +16 -3
  55. package/lib/subcommands/app/version.js.map +1 -1
  56. package/lib/util/parsing.d.ts +2 -0
  57. package/lib/util/parsing.d.ts.map +1 -0
  58. package/lib/util/parsing.js +17 -0
  59. package/lib/util/parsing.js.map +1 -0
  60. package/package.json +4 -6
  61. package/readme.md +146 -92
  62. package/src/application-control/environment-variables.test.ts +43 -3
  63. package/src/application-control/environment-variables.ts +29 -19
  64. package/src/application-control/install.ts +1 -1
  65. package/src/cloud-connection/device-agent-cloud-connection.ts +155 -141
  66. package/src/cloud-connection/live-updates-handler.test.ts +20 -0
  67. package/src/cloud-connection/live-updates-handler.ts +45 -52
  68. package/src/cloud-connection/messages.ts +1 -14
  69. package/src/cloud-connection/publisher.ts +4 -0
  70. package/src/cloud-connection/shadow-handler.test.ts +88 -28
  71. package/src/cloud-connection/shadow-handler.ts +13 -3
  72. package/src/cloud-connection/transaction-manager.test.ts +193 -18
  73. package/src/cloud-connection/transaction-manager.ts +174 -26
  74. package/src/subcommands/app/analytics.ts +99 -0
  75. package/src/subcommands/app/index.ts +4 -3
  76. package/src/subcommands/app/models.ts +13 -49
  77. package/src/subcommands/app/status.ts +20 -3
  78. package/src/subcommands/app/version.ts +19 -4
  79. package/src/util/parsing.ts +11 -0
  80. package/lib/cloud-connection/cmd-status.d.ts +0 -8
  81. package/lib/cloud-connection/cmd-status.d.ts.map +0 -1
  82. package/lib/cloud-connection/cmd-status.js +0 -62
  83. package/lib/cloud-connection/cmd-status.js.map +0 -1
  84. package/lib/cloud-connection/message-builder.d.ts +0 -7
  85. package/lib/cloud-connection/message-builder.d.ts.map +0 -1
  86. package/lib/cloud-connection/message-builder.js +0 -63
  87. package/lib/cloud-connection/message-builder.js.map +0 -1
  88. package/src/cloud-connection/cmd-status.ts +0 -71
  89. package/src/cloud-connection/message-builder.ts +0 -117
package/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- # alwaysAI Device Agent
1
+ # alwaysAI Device Agent <!-- omit from toc -->
2
2
 
3
3
  The alwaysAI Device Agent enables provisioning devices and managing devices and
4
4
  applications in production deployments of alwaysAI Computer Vision applications.
@@ -9,28 +9,60 @@ Note that the Device Agent is still in an experimental phase and these commands
9
9
  are likely to change. This guide will be updated with the latest usage as things
10
10
  change.
11
11
 
12
+ - [System Requirements \& Prerequisites](#system-requirements--prerequisites)
13
+ - [Provision Device](#provision-device)
14
+ - [Install the alwaysAI Device Agent and dependencies](#install-the-alwaysai-device-agent-and-dependencies)
15
+ - [Provision the device](#provision-the-device)
16
+ - [Run an alwaysAI application on your device](#run-an-alwaysai-application-on-your-device)
17
+ - [Enable Analytics through the alwaysAI Device Agent](#enable-analytics-through-the-alwaysai-device-agent)
18
+ - [Configure the Device Agent](#configure-the-device-agent)
19
+ - [Configure the Application](#configure-the-application)
20
+ - [Using the alwaysAI Console](#using-the-alwaysai-console)
21
+ - [In a docker-compose.yaml](#in-a-docker-composeyaml)
22
+ - [Enable Publishing to Cloud](#enable-publishing-to-cloud)
23
+ - [In your app source](#in-your-app-source)
24
+ - [From the alwaysAI Console](#from-the-alwaysai-console)
25
+ - [Capture the Analytics Stream](#capture-the-analytics-stream)
26
+ - [The alwaysAI Device Agent Command Line interface](#the-alwaysai-device-agent-command-line-interface)
27
+ - [Install the application on the device](#install-the-application-on-the-device)
28
+ - [Control the application](#control-the-application)
29
+ - [Manage application models](#manage-application-models)
30
+ - [Update models to new version of the same model ID](#update-models-to-new-version-of-the-same-model-id)
31
+ - [Replace models for an application for new ID](#replace-models-for-an-application-for-new-id)
32
+ - [Manually download a model package](#manually-download-a-model-package)
33
+ - [Set and update environment variables](#set-and-update-environment-variables)
34
+ - [Device Cleaning and Uninstalling](#device-cleaning-and-uninstalling)
35
+ - [Stop PM2 instance of the device agent](#stop-pm2-instance-of-the-device-agent)
36
+ - [Remove the device configuration](#remove-the-device-configuration)
37
+ - [Uninstall the Device Agent from the device](#uninstall-the-device-agent-from-the-device)
38
+ - [Remove the device from the alwaysAI Dashboard](#remove-the-device-from-the-alwaysai-dashboard)
39
+
40
+
12
41
  ## System Requirements & Prerequisites
13
42
 
14
43
  * Supported OS:
15
- * Debian Bullseye, Buster
16
- * Ubuntu 20.04, 18.04
17
- * NVIDIA Jetpack 4.6.x
44
+ * Debian Bookworm, Bullseye
45
+ * Ubuntu 23.10, 22.04, 20.04, 18.04
46
+ * NVIDIA Jetpack 5.1, 4.6.x
18
47
  * Supported target architecture
19
48
  * amd64
20
49
  * aarch64
21
- * armv7hf
22
- * `docker` version >= 19.03
50
+ * `docker` >= 19.03
51
+ * `docker-compose` >= 1.29.0; < 2.0.0
23
52
  * `curl` installed (required to download provisioning scripts)
24
53
  * Passwordless `sudo` for `npm` if using `pm2`
25
54
  * Passwordless `sudo` for `/sbin/shutdown` for device restart functionality
26
55
 
27
- To enable passwordless `sudo` for `npm` for the current user, run `sudo visudo`
28
- and add the following line to the end of the file:
56
+ To enable passwordless `sudo` for `npm` and `sbin/shutdown` for the current
57
+ user, run `sudo visudo` and add the following lines to the end of the file:
29
58
 
30
59
  ```bash
31
60
  <username> <hostname> = (root) NOPASSWD: /usr/bin/npm
61
+ <username> <hostname> = (root) NOPASSWD: /sbin/shutdown
32
62
  ```
33
63
 
64
+ On a linux system `username` can be obtained by typing `whoami` into the terminal. Similarly, if you don't know your `hostname` you can simply type `hostname`.
65
+
34
66
  ## Provision Device
35
67
 
36
68
  ### Install the alwaysAI Device Agent and dependencies
@@ -122,12 +154,14 @@ Now you can deploy to your device from the alwaysAI Dashboard.
122
154
  ## Enable Analytics through the alwaysAI Device Agent
123
155
 
124
156
  ### Configure the Device Agent
157
+
125
158
  You can send information from your device to the alwaysAI cloud securely using
126
159
  the Device Agent. These instructions assume you have provisioned your device
127
160
  using the default script parameters and have the Device agent running (i.e. the
128
161
  provisioning script was not run with the `--provision-only` flag set).
129
162
 
130
- First, the Device Agent must be configured to enable Analytics Pass-through support. Confirm that the `ALWAYSAI_ANALYTICS_PASSTHROUGH` environment variable is set:
163
+ The Device Agent must be configured to enable Analytics Pass-through support.
164
+ Confirm that the `ALWAYSAI_ANALYTICS_PASSTHROUGH` environment variable is set:
131
165
 
132
166
  ```bash
133
167
  $ pm2 env 0 | grep ALWAYSAI
@@ -147,31 +181,38 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS
147
181
 
148
182
  ### Configure the Application
149
183
 
150
- From the application side, the `ALWAYSAI_CONNECT_TO_DEVICE_AGENT` environment
151
- variable must be set. There are a few options:
152
-
153
- #### In the app source
154
-
155
- In your `app.py` file, make sure the top of your import statements looks like
156
- this:
184
+ First, you must publish analytics from your application using
185
+ `publish_analytics`. Whenever this command is used, it will publish the contents
186
+ of the analytics message to the analytics endpoints you have enabled. Each core
187
+ computer vision service has it's own `publish_analytics` method, which is called
188
+ on the instance of the class. Or, you can published any JSON-serializable
189
+ message with `edgeiq.publish_analytics()`. For instance, to publish object
190
+ detection results you can use:
157
191
 
158
192
  ```python
159
- import os
160
- os.environ['ALWAYSAI_CONNECT_TO_DEVICE_AGENT']='1'
161
- import edgeiq
193
+ results = obj_detect.detect_objects(frame, confidence_level=.5)
194
+ try:
195
+ obj_detect.publish_analytics(results, tag=frame_count)
196
+ except edgeiq.PublishError as e:
197
+ print(e)
162
198
  ```
163
- You can import any other modules after `edgeiq`, but the other orders must be
164
- maintained.
165
199
 
166
- #### In the Dockerfile
200
+ Next, the `ALWAYSAI_CONNECT_TO_DEVICE_AGENT` environment variable must be set.
201
+ There are a few options:
167
202
 
168
- ```bash
169
- ENV ALWAYSAI_CONNECT_TO_DEVICE_AGENT=1
170
- ```
203
+ #### Using the alwaysAI Console
204
+
205
+ Once an app has been deployed to a device, you can add, modify, and remove
206
+ environment variables using the environment variable tab of the app details
207
+ panel. Add a new environment variable called `ALWAYSAI_CONNECT_TO_DEVICE_AGENT`
208
+ and set it to `1`.
171
209
 
172
210
  #### In a docker-compose.yaml
173
211
 
174
- Add the following section to the service for your app:
212
+ Add the following section to the service for your app in a `docker-compose.yaml`
213
+ file. If you don't yet have a `docker-compose.yaml` file in your app source, you
214
+ can generate a template one to edit by running the `aai app generate docker-compose`
215
+ command. Note that this will only take effect in production deployments.
175
216
 
176
217
  ```bash
177
218
  environment:
@@ -180,12 +221,31 @@ environment:
180
221
 
181
222
  ### Enable Publishing to Cloud
182
223
 
183
- To enable cloud publishing for your application you can either run
184
- `aai app enable-cloud-publish`, or update your `alwaysai.app.json` by adding the
185
- following component in addition to any `models` or `scripts` components:
224
+ The application analytics configuration can be set either in the app source when
225
+ you publish it or once the app has been deployed to a device using the alwaysAI
226
+ Console.
227
+
228
+ #### In your app source
229
+
230
+ To enable cloud publishing in your app configurations run the following commands
231
+ in your app directory:
232
+
233
+ ```bash
234
+ $ aai app enable-cloud-publish
235
+ $ aai app publish
236
+ ```
237
+
238
+ This can be added to the app release process so that cloud publishing is always
239
+ enabled for production deployments.
240
+
241
+ #### From the alwaysAI Console
242
+
243
+ In the alwaysAI Console, navigate to the device the application has been
244
+ deployed to and open the configuration tab in the app panel. In the app config,
245
+ add an `analytics` section that looks like the following:
186
246
 
187
247
  ```json
188
- "analytics: {
248
+ "analytics": {
189
249
  "enable_cloud_publish": true
190
250
  }
191
251
  ```
@@ -206,30 +266,12 @@ So, a valid `alwaysai.app.json` for publishing analytics might look like this:
206
266
  }
207
267
  ```
208
268
 
209
- Finally, add a command to publish analytics to your `app.py`. Whenever this
210
- command is used, it will publish the contents of the analytics message to the
211
- cloud -- you can choose to call this every frame, or once every event
212
- occurrence, however your app is designed. Each core computer vision service has
213
- it's own `publish_analytics` method, which is called on the instance of the
214
- class. Or, you can published a JSON-serializable message with
215
- `edgeiq.publish_analytics()`. For instance, to publish object detection results
216
- you can use:
269
+ Press "Update" to update the application configuration on the device.
217
270
 
218
- ```python
219
- results = obj_detect.detect_objects(frame, confidence_level=.5)
220
- try:
221
- obj_detect.publish_analytics(results, tag=frame_count)
222
- except edgeiq.PublishError as e:
223
- print(e)
224
- ```
225
-
226
- Finally, make sure to save all of your changes, and publish your application with
227
-
228
- ```bash
229
- $ aai app publish
230
- ```
271
+ ### Capture the Analytics Stream
231
272
 
232
- You can test that analytics are being viewed with `wscat`, using your application's project ID and a secure API key:
273
+ You can test that analytics are being viewed with `wscat`, using your
274
+ application's project ID and a secure API key:
233
275
 
234
276
  ```bash
235
277
  $ wscat -c "wss://analytics.alwaysai.co?projectId=[PROJECT_ID]&apiKey=[API_KEY]"
@@ -239,7 +281,8 @@ Please contact the alwaysAI team if you need a secure API key.
239
281
 
240
282
  ## The alwaysAI Device Agent Command Line interface
241
283
 
242
- The Device Agent can also be used directly on the device with it's command line interface.
284
+ The Device Agent can also be used directly on the device with it's command line
285
+ interface.
243
286
 
244
287
  ```bash
245
288
  $ aai-agent --help
@@ -249,30 +292,29 @@ Usage: aai-agent <subcommand> ...
249
292
 
250
293
  Subcommands:
251
294
 
252
- login : Login to alwaysAI (this is meant for scripted environments)
253
- app list : List all installed apps
254
- app install : Install an alwaysAI app from a project
255
- app status : Get the status of an installed alwaysAI app
256
- app start : Start an installed alwaysAI app
257
- app stop : Stop a running alwaysAI app
258
- app restart : Restart running alwaysAI app
259
- app logs : Get logs for an application
260
- app uninstall : Remove an alwaysAI app
261
- app show-models : Show the application models
262
- app add-model : Add a model to an alwaysAI app
263
- app remove-model : Remove a model from an alwaysAI app
264
- app get-all-envs : Get environment variables for an application
265
- app set-env : Set environment variables for a service
266
- device init : Initialize device
267
- device get-info : Get device info
268
- device clean : Remove all provisioning files
269
- get-model-package : Download and unpack a model package
270
- ```
271
-
272
- To see the output logs, run the following command:
273
-
274
- ```bash
275
- $ export ALWAYSAI_LOG_TO_CONSOLE=1
295
+ login : Login to alwaysAI (this is meant for scripted environments)
296
+ app list : List all installed apps
297
+ app install : Install an alwaysAI app from a project
298
+ app status : Get the status of an installed alwaysAI app
299
+ app start : Start an installed alwaysAI app
300
+ app stop : Stop a running alwaysAI app
301
+ app restart : Restart running alwaysAI app
302
+ app logs : Get logs for an application
303
+ app uninstall : Remove an alwaysAI app
304
+ app show-models : Show the application models
305
+ app add-model : Add a model to an alwaysAI app
306
+ app remove-model : Remove a model from an alwaysAI app
307
+ app get-all-envs : Get environment variables for an application
308
+ app set-env : Set environment variables for a service
309
+ app get-analytics-cfg : Get analytics configuration for an application
310
+ app set-analytics-cfg : Set analytics configuration for an application. Note that this resets the config so all desired options must be set
311
+ app get-shadow : Get the current shadow
312
+ app update-shadow : Update the shadow with the current application configuration
313
+ device init : Initialize device
314
+ device get-info : Get device info
315
+ device clean : Remove all provisioning files
316
+ device restart : Restart the device
317
+ get-model-package : Download and unpack a model package
276
318
  ```
277
319
 
278
320
  ### Install the application on the device
@@ -350,7 +392,8 @@ obj_detect = edgeiq.ObjectDetection(edgeiq._globals.MODEL_ID_LIST[0])
350
392
 
351
393
  #### Manually download a model package
352
394
 
353
- To download a model package from the alwaysAI cloud and unpack to a specific directory, run:
395
+ To download a model package from the alwaysAI cloud and unpack to a specific
396
+ directory, run:
354
397
 
355
398
  ```bash
356
399
  $ aai-agent get-model-package <model ID> [--path <destination path>]
@@ -380,11 +423,16 @@ service run the following command on the device:
380
423
  ```bash
381
424
  $ aai-agent app set-env TEST_ENV=1 --project <project_id> --service alwaysai
382
425
  ```
426
+
383
427
  ## Device Cleaning and Uninstalling
428
+
384
429
  In order to fully clean the device and uninstall the agent, use the following steps:
385
430
 
386
- ### 1. Stop PM2 instance of the device agent
387
- Using `pm2 list`, display the list of current pm2 instances. The output should look like this:
431
+ ### Stop PM2 instance of the device agent
432
+
433
+ Using `pm2 list`, display the list of current pm2 instances. The output should
434
+ look like this:
435
+
388
436
  ```bash
389
437
  $ pm2 list
390
438
  ┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
@@ -393,19 +441,25 @@ $ pm2 list
393
441
  │ 0 │ aai-agent │ fork │ 15 │ online │ 0% │ 2.8mb │
394
442
  └────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
395
443
  ```
396
- Start with `pm2 stop <id>` where `<id>` is the id of the aai-agent.
397
- Then, run `pm2 delete 0`.
398
- Follow it with these two commands:
399
- `pm2 flush`, `pm2 unstartup`
444
+ Run the following commands where `<id>` is the ID for `aai-agent`.
445
+
446
+ ```bash
447
+ $ pm2 stop <id>
448
+ $ pm2 delete 0
449
+ $ pm2 flush
450
+ $ pm2 unstartup
451
+ ```
452
+ ### Remove the device configuration
453
+
454
+ Open a new terminal and run `aai-agent device clean`.
400
455
 
401
- ### 2. Remove alwaysAI Provisioned Device
402
- Open a new terminal.
403
- Run `aai-agent device clean`
456
+ ### Uninstall the Device Agent from the device
404
457
 
405
- ### 3. Uninstall aai Device Agent from the device
406
- Run `sudo npm uninstall -g @alwaysai/device-agent`
407
- Verify successful removal by running `aai-agent`. You should see a response similar to `-bash: /usr/bin/aai-agent: No such file or directory`.
458
+ Run `sudo npm uninstall -g @alwaysai/device-agent`. Verify successful removal by
459
+ running `aai-agent`. You should see a response similar to
460
+ `-bash:/usr/bin/aai-agent: No such file or directory`.
408
461
 
409
- ### 4. Remove the device from the aai Dashboard
410
- Go to https://console.alwaysai.co/dashboard/devices, Go to Devices, find the device in question, and remove the device using the trashcan icon.
462
+ ### Remove the device from the alwaysAI Dashboard
411
463
 
464
+ Go to the [Devices](https://console.alwaysai.co/dashboard/devices) tab, find the
465
+ device in question, and remove the device using the trashcan icon.
@@ -1,6 +1,10 @@
1
1
  import { AgentConfigFile } from '../infrastructure/agent-config';
2
2
  import { readDockerCompose, writeDockerCompose } from './config';
3
- import { getAllEnvs, setEnv } from './environment-variables';
3
+ import {
4
+ convertStringEnvsToKeyVal,
5
+ getAllEnvs,
6
+ setEnv
7
+ } from './environment-variables';
4
8
  import { isAppStarted, restartApp } from './status';
5
9
  import { buildApp, requireAppReady } from './utils';
6
10
 
@@ -132,7 +136,7 @@ describe('Test environment variable get and set', () => {
132
136
  dockerCompose: {
133
137
  services: {
134
138
  alwaysai: {
135
- environment: ['TEST1=1', 'TEST2=2', 'TEST1=2']
139
+ environment: ['TEST1=2', 'TEST2=2']
136
140
  }
137
141
  }
138
142
  }
@@ -158,7 +162,7 @@ describe('Test environment variable get and set', () => {
158
162
  dockerCompose: {
159
163
  services: {
160
164
  alwaysai: {
161
- environment: ['TEST1=3', 'TEST2=4', 'TEST1=2']
165
+ environment: ['TEST1=2', 'TEST2=4']
162
166
  },
163
167
  other: {
164
168
  environment: ['OTHER_TEST=1']
@@ -167,5 +171,41 @@ describe('Test environment variable get and set', () => {
167
171
  }
168
172
  });
169
173
  });
174
+ test('Remove one env', async () => {
175
+ const environment = ['TEST1=1', 'TEST2=2'];
176
+ const compose = {
177
+ services: {
178
+ alwaysai: { environment }
179
+ }
180
+ };
181
+ jest.mocked(readDockerCompose).mockResolvedValue(compose);
182
+
183
+ const envVars = { alwaysai: { TEST1: null } };
184
+ await setEnv({ projectId: projectId1, envVars });
185
+ expect(jest.mocked(readDockerCompose)).toBeCalledWith({
186
+ projectId: projectId1
187
+ });
188
+ expect(jest.mocked(writeDockerCompose)).toBeCalledWith({
189
+ projectId: projectId1,
190
+ dockerCompose: {
191
+ services: {
192
+ alwaysai: {
193
+ environment: ['TEST1=', 'TEST2=2']
194
+ }
195
+ }
196
+ }
197
+ });
198
+ });
199
+ });
200
+ describe('Test helpers', () => {
201
+ test('convertStringEnvsToKeyVal', () => {
202
+ const stringEnvs: string[] = ['TEST1=', 'TEST2=test2', 'TEST3=null'];
203
+ const envVars = convertStringEnvsToKeyVal(stringEnvs);
204
+ expect(envVars).toEqual({
205
+ TEST1: '',
206
+ TEST2: 'test2',
207
+ TEST3: 'null'
208
+ });
209
+ });
170
210
  });
171
211
  });
@@ -4,6 +4,7 @@ import { buildApp, getAppDir, requireAppReady } from './utils';
4
4
  import { logger } from '../util/logger';
5
5
  import { AgentConfigFile } from '../infrastructure/agent-config';
6
6
  import { isAppStarted, restartApp } from './status';
7
+ import { replaceFalseyWithNull } from '../util/parsing';
7
8
 
8
9
  export interface EnvVars {
9
10
  [service: string]: {
@@ -37,26 +38,32 @@ export async function setEnv(props: { projectId: string; envVars: EnvVars }) {
37
38
  )}`
38
39
  );
39
40
  }
40
- const envVarList: string[] = [];
41
+
42
+ const service = composeParsed['services'][s];
43
+ const oldEnv: string[] | undefined = service['environment'];
44
+
45
+ const newEnvVarsObj = {};
46
+ oldEnv?.forEach((envVarStr: string) => {
47
+ const envVarSplit = envVarStr.split('=');
48
+ const key = envVarSplit[0];
49
+ const value = envVarSplit[1];
50
+ newEnvVarsObj[key] = value;
51
+ });
52
+
41
53
  for (const envVar of Object.keys(envVars[s])) {
42
- envVarList.push(`${envVar}=${envVars[s][envVar]}`);
54
+ newEnvVarsObj[envVar] = envVars[s][envVar];
43
55
  }
44
- const service = composeParsed['services'][s];
45
- // The environment field overrides the env files, so by appending to
46
- // the environment list we can assure the value will be used over
47
- // the env_file.
48
-
49
- // NOTE: We are only appending the new values to the end of the
50
- // environment variable list, which will override but not replace
51
- // previous instances of the same environment variable.
52
- if ('environment' in service) {
53
- const environment: string[] = service['environment'];
54
- composeParsed['services'][s]['environment'] =
55
- environment.concat(envVarList);
56
- } else {
57
- composeParsed['services'][s]['environment'] = envVarList;
56
+
57
+ const envVarList: string[] = [];
58
+ for (const envVar of Object.keys(newEnvVarsObj)) {
59
+ envVarList.push(
60
+ `${envVar}=${newEnvVarsObj[envVar] ? newEnvVarsObj[envVar] : ''}`
61
+ );
58
62
  }
63
+
64
+ service['environment'] = envVarList;
59
65
  }
66
+
60
67
  await writeDockerCompose({ projectId, dockerCompose: composeParsed });
61
68
 
62
69
  const appDir = getAppDir(projectId);
@@ -79,7 +86,7 @@ export async function setEnv(props: { projectId: string; envVars: EnvVars }) {
79
86
  );
80
87
  }
81
88
 
82
- async function convertStringEnvsToKeyVal(stringEnvs: string[]) {
89
+ export function convertStringEnvsToKeyVal(stringEnvs: string[]) {
83
90
  const envVars = {};
84
91
  stringEnvs.forEach((env: string) => {
85
92
  const keyVal = env.split('=');
@@ -117,17 +124,20 @@ export async function getAllEnvs(props: {
117
124
  envFileLines = envFileLines.filter((v) => {
118
125
  return v !== '' && !v.includes('#');
119
126
  });
120
- const newEnvVars = await convertStringEnvsToKeyVal(envFileLines);
127
+ const newEnvVars = convertStringEnvsToKeyVal(envFileLines);
121
128
  envVars[s] = { ...envVars[s], ...newEnvVars };
122
129
  }
123
130
  }
124
131
  if ('environment' in service) {
125
132
  const environment: string[] = service['environment'];
126
- const newEnvVars = await convertStringEnvsToKeyVal(environment);
133
+ const newEnvVars = convertStringEnvsToKeyVal(environment);
127
134
  envVars[s] = { ...envVars[s], ...newEnvVars };
128
135
  }
129
136
  }
130
137
  }
131
138
 
139
+ // Device shadow needs null to delete
140
+ replaceFalseyWithNull(envVars, true);
141
+
132
142
  return envVars;
133
143
  }
@@ -65,7 +65,7 @@ export async function installApp(props: {
65
65
  if (!(await AgentConfigFile().isAppReady({ projectId }))) {
66
66
  throw new Error('Application already has installation in progress!');
67
67
  }
68
- logger.info('Application is already installed, updating');
68
+ logger.info('Application is already installed, updating...');
69
69
 
70
70
  // make sure the app is stopped if it's running, to clear the docker container.
71
71
  if (await isAppStarted({ projectId })) {