@alwaysai/device-agent 0.0.1-2.1-beta-provision
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.
- package/lib/application-control/backup.d.ts +8 -0
- package/lib/application-control/backup.d.ts.map +1 -0
- package/lib/application-control/backup.js +37 -0
- package/lib/application-control/backup.js.map +1 -0
- package/lib/application-control/config.d.ts +17 -0
- package/lib/application-control/config.d.ts.map +1 -0
- package/lib/application-control/config.js +62 -0
- package/lib/application-control/config.js.map +1 -0
- package/lib/application-control/environment-variables.d.ts +9 -0
- package/lib/application-control/environment-variables.d.ts.map +1 -0
- package/lib/application-control/environment-variables.js +73 -0
- package/lib/application-control/environment-variables.js.map +1 -0
- package/lib/application-control/index.d.ts +10 -0
- package/lib/application-control/index.d.ts.map +1 -0
- package/lib/application-control/index.js +32 -0
- package/lib/application-control/index.js.map +1 -0
- package/lib/application-control/install.d.ts +22 -0
- package/lib/application-control/install.d.ts.map +1 -0
- package/lib/application-control/install.js +156 -0
- package/lib/application-control/install.js.map +1 -0
- package/lib/application-control/models.d.ts +23 -0
- package/lib/application-control/models.d.ts.map +1 -0
- package/lib/application-control/models.js +154 -0
- package/lib/application-control/models.js.map +1 -0
- package/lib/application-control/status.d.ts +27 -0
- package/lib/application-control/status.d.ts.map +1 -0
- package/lib/application-control/status.js +153 -0
- package/lib/application-control/status.js.map +1 -0
- package/lib/application-control/types.d.ts +5 -0
- package/lib/application-control/types.d.ts.map +1 -0
- package/lib/application-control/types.js +3 -0
- package/lib/application-control/types.js.map +1 -0
- package/lib/application-control/utils.d.ts +14 -0
- package/lib/application-control/utils.d.ts.map +1 -0
- package/lib/application-control/utils.js +82 -0
- package/lib/application-control/utils.js.map +1 -0
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +51 -0
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -0
- package/lib/cloud-connection/device-agent-cloud-connection.js +490 -0
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -0
- package/lib/cloud-connection/device-agent.d.ts +21 -0
- package/lib/cloud-connection/device-agent.d.ts.map +1 -0
- package/lib/cloud-connection/device-agent.js +65 -0
- package/lib/cloud-connection/device-agent.js.map +1 -0
- package/lib/device-control/device-control.d.ts +4 -0
- package/lib/device-control/device-control.d.ts.map +1 -0
- package/lib/device-control/device-control.js +20 -0
- package/lib/device-control/device-control.js.map +1 -0
- package/lib/docker/docker-cmd.d.ts +4 -0
- package/lib/docker/docker-cmd.d.ts.map +1 -0
- package/lib/docker/docker-cmd.js +16 -0
- package/lib/docker/docker-cmd.js.map +1 -0
- package/lib/docker/docker-compose-cmd.d.ts +5 -0
- package/lib/docker/docker-compose-cmd.d.ts.map +1 -0
- package/lib/docker/docker-compose-cmd.js +16 -0
- package/lib/docker/docker-compose-cmd.js.map +1 -0
- package/lib/endpoints.d.ts +3 -0
- package/lib/endpoints.d.ts.map +1 -0
- package/lib/endpoints.js +28 -0
- package/lib/endpoints.js.map +1 -0
- package/lib/environment.d.ts +7 -0
- package/lib/environment.d.ts.map +1 -0
- package/lib/environment.js +24 -0
- package/lib/environment.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +26 -0
- package/lib/index.js.map +1 -0
- package/lib/infrastructure/agent-config.d.ts +73 -0
- package/lib/infrastructure/agent-config.d.ts.map +1 -0
- package/lib/infrastructure/agent-config.js +175 -0
- package/lib/infrastructure/agent-config.js.map +1 -0
- package/lib/infrastructure/agent-config.test.d.ts +2 -0
- package/lib/infrastructure/agent-config.test.d.ts.map +1 -0
- package/lib/infrastructure/agent-config.test.js +182 -0
- package/lib/infrastructure/agent-config.test.js.map +1 -0
- package/lib/infrastructure/certificates-and-tokens.d.ts +6 -0
- package/lib/infrastructure/certificates-and-tokens.d.ts.map +1 -0
- package/lib/infrastructure/certificates-and-tokens.js +37 -0
- package/lib/infrastructure/certificates-and-tokens.js.map +1 -0
- package/lib/infrastructure/urls.d.ts +2 -0
- package/lib/infrastructure/urls.d.ts.map +1 -0
- package/lib/infrastructure/urls.js +25 -0
- package/lib/infrastructure/urls.js.map +1 -0
- package/lib/root.d.ts +2 -0
- package/lib/root.d.ts.map +1 -0
- package/lib/root.js +12 -0
- package/lib/root.js.map +1 -0
- package/lib/subcommands/app/app.d.ts +59 -0
- package/lib/subcommands/app/app.d.ts.map +1 -0
- package/lib/subcommands/app/app.js +317 -0
- package/lib/subcommands/app/app.js.map +1 -0
- package/lib/subcommands/app/index.d.ts +2 -0
- package/lib/subcommands/app/index.d.ts.map +1 -0
- package/lib/subcommands/app/index.js +30 -0
- package/lib/subcommands/app/index.js.map +1 -0
- package/lib/subcommands/device/clean.d.ts +2 -0
- package/lib/subcommands/device/clean.d.ts.map +1 -0
- package/lib/subcommands/device/clean.js +20 -0
- package/lib/subcommands/device/clean.js.map +1 -0
- package/lib/subcommands/device/device.d.ts +6 -0
- package/lib/subcommands/device/device.d.ts.map +1 -0
- package/lib/subcommands/device/device.js +92 -0
- package/lib/subcommands/device/device.js.map +1 -0
- package/lib/subcommands/device/index.d.ts +2 -0
- package/lib/subcommands/device/index.d.ts.map +1 -0
- package/lib/subcommands/device/index.js +12 -0
- package/lib/subcommands/device/index.js.map +1 -0
- package/lib/subcommands/get-model-package.d.ts +5 -0
- package/lib/subcommands/get-model-package.d.ts.map +1 -0
- package/lib/subcommands/get-model-package.js +35 -0
- package/lib/subcommands/get-model-package.js.map +1 -0
- package/lib/subcommands/index.d.ts +9 -0
- package/lib/subcommands/index.d.ts.map +1 -0
- package/lib/subcommands/index.js +14 -0
- package/lib/subcommands/index.js.map +1 -0
- package/lib/subcommands/login.d.ts +6 -0
- package/lib/subcommands/login.d.ts.map +1 -0
- package/lib/subcommands/login.js +35 -0
- package/lib/subcommands/login.js.map +1 -0
- package/lib/util/clean-certs.d.ts +2 -0
- package/lib/util/clean-certs.d.ts.map +1 -0
- package/lib/util/clean-certs.js +16 -0
- package/lib/util/clean-certs.js.map +1 -0
- package/lib/util/copy-dir.d.ts +5 -0
- package/lib/util/copy-dir.d.ts.map +1 -0
- package/lib/util/copy-dir.js +16 -0
- package/lib/util/copy-dir.js.map +1 -0
- package/lib/util/directories.d.ts +23 -0
- package/lib/util/directories.d.ts.map +1 -0
- package/lib/util/directories.js +50 -0
- package/lib/util/directories.js.map +1 -0
- package/lib/util/get-device-id.d.ts +2 -0
- package/lib/util/get-device-id.d.ts.map +1 -0
- package/lib/util/get-device-id.js +24 -0
- package/lib/util/get-device-id.js.map +1 -0
- package/lib/util/http-client.d.ts +3 -0
- package/lib/util/http-client.d.ts.map +1 -0
- package/lib/util/http-client.js +30 -0
- package/lib/util/http-client.js.map +1 -0
- package/lib/util/logger.d.ts +4 -0
- package/lib/util/logger.d.ts.map +1 -0
- package/lib/util/logger.js +24 -0
- package/lib/util/logger.js.map +1 -0
- package/lib/util/run-in-dir.d.ts +2 -0
- package/lib/util/run-in-dir.d.ts.map +1 -0
- package/lib/util/run-in-dir.js +17 -0
- package/lib/util/run-in-dir.js.map +1 -0
- package/lib/util/sleep.d.ts +2 -0
- package/lib/util/sleep.d.ts.map +1 -0
- package/lib/util/sleep.js +9 -0
- package/lib/util/sleep.js.map +1 -0
- package/package.json +98 -0
- package/readme.md +219 -0
- package/src/application-control/backup.ts +36 -0
- package/src/application-control/config.ts +61 -0
- package/src/application-control/environment-variables.ts +74 -0
- package/src/application-control/index.ts +45 -0
- package/src/application-control/install.ts +206 -0
- package/src/application-control/models.ts +194 -0
- package/src/application-control/status.ts +187 -0
- package/src/application-control/types.ts +1 -0
- package/src/application-control/utils.ts +95 -0
- package/src/cloud-connection/device-agent-cloud-connection.ts +673 -0
- package/src/cloud-connection/device-agent.ts +120 -0
- package/src/device-control/device-control.ts +16 -0
- package/src/docker/docker-cmd.ts +12 -0
- package/src/docker/docker-compose-cmd.ts +12 -0
- package/src/endpoints.ts +24 -0
- package/src/environment.ts +28 -0
- package/src/index.ts +26 -0
- package/src/infrastructure/agent-config.test.ts +199 -0
- package/src/infrastructure/agent-config.ts +208 -0
- package/src/infrastructure/certificates-and-tokens.ts +47 -0
- package/src/infrastructure/urls.ts +21 -0
- package/src/root.ts +11 -0
- package/src/subcommands/app/app.ts +337 -0
- package/src/subcommands/app/index.ts +46 -0
- package/src/subcommands/device/clean.ts +16 -0
- package/src/subcommands/device/device.ts +126 -0
- package/src/subcommands/device/index.ts +9 -0
- package/src/subcommands/get-model-package.ts +33 -0
- package/src/subcommands/index.ts +11 -0
- package/src/subcommands/login.ts +33 -0
- package/src/util/clean-certs.ts +12 -0
- package/src/util/copy-dir.ts +12 -0
- package/src/util/directories.ts +82 -0
- package/src/util/get-device-id.ts +22 -0
- package/src/util/http-client.ts +35 -0
- package/src/util/logger.ts +28 -0
- package/src/util/run-in-dir.ts +15 -0
- package/src/util/sleep.ts +5 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as rimraf from 'rimraf';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { JsSpawner, Spawner } from 'alwaysai/lib/util';
|
|
5
|
+
import { getAppDir, downloadPackageUsingPresignedUrl, buildApp } from './utils';
|
|
6
|
+
import { AppDetailsPacket, ModelInstallPayload } from '@alwaysai/device-agent-schemas';
|
|
7
|
+
import { BACKUP_EXT, createAppBackup } from './backup';
|
|
8
|
+
import { stopApp } from './status';
|
|
9
|
+
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
10
|
+
import { ProjectJsonFile } from 'alwaysai/lib/core/project';
|
|
11
|
+
import {
|
|
12
|
+
getTargetHardwareType,
|
|
13
|
+
AppJsonFile,
|
|
14
|
+
TargetJsonFile,
|
|
15
|
+
} from 'alwaysai/lib/core/app';
|
|
16
|
+
import { appCleanDockerComponent, buildDocker } from 'alwaysai/lib/components/app';
|
|
17
|
+
import {
|
|
18
|
+
appInstallComponent,
|
|
19
|
+
installVenv,
|
|
20
|
+
} from 'alwaysai/lib/components/app/app-install-component';
|
|
21
|
+
import { DOCKERFILE, DOCKER_IMAGE_ID_INITIAL_VALUE } from 'alwaysai/lib/constants';
|
|
22
|
+
import { appReleasePullComponent } from 'alwaysai/lib/components/release';
|
|
23
|
+
import { runInDir } from '../util/run-in-dir';
|
|
24
|
+
import { installModelsWithPresignedURLs } from './models';
|
|
25
|
+
import { logger } from '../util/logger';
|
|
26
|
+
|
|
27
|
+
type SignedUrlPayloadType = {
|
|
28
|
+
appInstallPayload: {
|
|
29
|
+
appSignedUrl: string;
|
|
30
|
+
};
|
|
31
|
+
modelsInstallPayload: {
|
|
32
|
+
id: string;
|
|
33
|
+
version: number;
|
|
34
|
+
modelSignedUrl: string;
|
|
35
|
+
}[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function getInstalledApps(): Promise<AppDetailsPacket[]> {
|
|
39
|
+
const apps = await AgentConfigFile().getApps();
|
|
40
|
+
const appDetails: AppDetailsPacket[] = [];
|
|
41
|
+
for (const app of apps) {
|
|
42
|
+
const { projectId, version } = app;
|
|
43
|
+
appDetails.push({ projectId, version });
|
|
44
|
+
}
|
|
45
|
+
return appDetails;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function installApp(props: {
|
|
49
|
+
projectId: string;
|
|
50
|
+
appReleaseHash: string;
|
|
51
|
+
signedUrlsPayload?: SignedUrlPayloadType;
|
|
52
|
+
}): Promise<void> {
|
|
53
|
+
const { projectId, appReleaseHash, signedUrlsPayload } = props;
|
|
54
|
+
|
|
55
|
+
const appDir = getAppDir(projectId);
|
|
56
|
+
const spawner = JsSpawner();
|
|
57
|
+
if (await AgentConfigFile().isAppPresent({ projectId })) {
|
|
58
|
+
if (!(await AgentConfigFile().isAppReady({ projectId }))) {
|
|
59
|
+
throw new Error('Application already has installation in progress!');
|
|
60
|
+
}
|
|
61
|
+
logger.info('Application is already installed, updating');
|
|
62
|
+
await AgentConfigFile().setAppInstalling({ projectId, version: appReleaseHash });
|
|
63
|
+
await spawner.rimraf(appDir);
|
|
64
|
+
} else {
|
|
65
|
+
await AgentConfigFile().setAppInstalling({ projectId, version: appReleaseHash });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await spawner.mkdirp(appDir);
|
|
69
|
+
|
|
70
|
+
// download app package
|
|
71
|
+
const localDest = path.join(appDir, `${path.basename(appReleaseHash)}.tgz`);
|
|
72
|
+
if (!signedUrlsPayload) {
|
|
73
|
+
await runInDir(
|
|
74
|
+
appReleasePullComponent,
|
|
75
|
+
[
|
|
76
|
+
{
|
|
77
|
+
yes: true,
|
|
78
|
+
project: projectId,
|
|
79
|
+
releaseHash: appReleaseHash,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
appDir,
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
const { appSignedUrl } = signedUrlsPayload.appInstallPayload;
|
|
86
|
+
await downloadPackageUsingPresignedUrl({ localDest, presignedUrl: appSignedUrl });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unpack app package and remove tar file
|
|
90
|
+
await unPackApp({ spawner, localDest, appDir });
|
|
91
|
+
|
|
92
|
+
// The following changes replace the runCliCmd for 'app configure'
|
|
93
|
+
// This part replaces findOrWriteProjectJsonFile() due to the
|
|
94
|
+
// calls that check the user is logged in and also for project collaboration
|
|
95
|
+
// NOTE: this process no longer checks project collaboration
|
|
96
|
+
await checkValidProjectFiles({ appDir });
|
|
97
|
+
|
|
98
|
+
// install models, python venv, and docker image
|
|
99
|
+
if (!signedUrlsPayload) {
|
|
100
|
+
await runInDir(
|
|
101
|
+
appInstallComponent,
|
|
102
|
+
[
|
|
103
|
+
{
|
|
104
|
+
yes: true,
|
|
105
|
+
pull: true,
|
|
106
|
+
clean: true,
|
|
107
|
+
skipModels: false,
|
|
108
|
+
source: false,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
appDir,
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
await installAppBuildReqs({ appDir });
|
|
115
|
+
await installModelsWithPresignedURLs(
|
|
116
|
+
signedUrlsPayload.modelsInstallPayload,
|
|
117
|
+
path.join(appDir, 'models'),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await buildApp({ appDir });
|
|
122
|
+
|
|
123
|
+
await AgentConfigFile().setAppInstalled({ projectId, version: appReleaseHash });
|
|
124
|
+
|
|
125
|
+
logger.info(
|
|
126
|
+
`Installed app version ${appReleaseHash} from project ${projectId} to ${appDir}.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function installAppBuildReqs(props: { appDir: string }) {
|
|
131
|
+
const { appDir } = props;
|
|
132
|
+
const targetJsonFile = TargetJsonFile(appDir);
|
|
133
|
+
const targetJson = targetJsonFile.readIfExists();
|
|
134
|
+
if (!targetJson) {
|
|
135
|
+
throw new Error('Target json file does not exist!');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await runInDir(
|
|
139
|
+
async () => {
|
|
140
|
+
const targetHostSpawner = targetJsonFile.readHostSpawner();
|
|
141
|
+
|
|
142
|
+
await appCleanDockerComponent({ targetHostSpawner });
|
|
143
|
+
|
|
144
|
+
await buildDocker({
|
|
145
|
+
targetJson,
|
|
146
|
+
targetJsonFile,
|
|
147
|
+
targetHostSpawner,
|
|
148
|
+
pull: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await installVenv({
|
|
152
|
+
targetJson,
|
|
153
|
+
sourceSpawner: targetHostSpawner,
|
|
154
|
+
targetJsonFile,
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
[],
|
|
158
|
+
appDir,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function unPackApp(props: { spawner: Spawner; localDest: string; appDir: string }) {
|
|
163
|
+
const { spawner, localDest, appDir } = props;
|
|
164
|
+
await spawner.untar(fs.createReadStream(localDest), appDir);
|
|
165
|
+
await spawner.rimraf(localDest);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function checkValidProjectFiles({ appDir }) {
|
|
169
|
+
if (!ProjectJsonFile(appDir).readIfExists()) {
|
|
170
|
+
throw new Error('Project JSON file does not exist!');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// check for app json file and app.py files
|
|
174
|
+
if (!AppJsonFile(appDir).readIfExists()) {
|
|
175
|
+
throw new Error('App JSON file does not exist!');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// write target json
|
|
179
|
+
if (!fs.existsSync(path.join(appDir, DOCKERFILE))) {
|
|
180
|
+
throw new Error('No Dockerfile found for application!');
|
|
181
|
+
}
|
|
182
|
+
TargetJsonFile(appDir).write({
|
|
183
|
+
targetProtocol: 'docker:',
|
|
184
|
+
dockerImageId: DOCKER_IMAGE_ID_INITIAL_VALUE,
|
|
185
|
+
targetHardware: await getTargetHardwareType({}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function uninstallApp(props: { projectId: string }): Promise<void> {
|
|
190
|
+
const { projectId } = props;
|
|
191
|
+
if (!(await AgentConfigFile().isAppPresent({ projectId }))) {
|
|
192
|
+
logger.info(`Application ${projectId} not installed`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await stopApp({ projectId });
|
|
197
|
+
} catch {
|
|
198
|
+
logger.info('Failed to stop app, may be left running...');
|
|
199
|
+
}
|
|
200
|
+
await AgentConfigFile().setAppUninstalled({ projectId });
|
|
201
|
+
// Delete application directory and backup
|
|
202
|
+
const appDir = getAppDir(projectId);
|
|
203
|
+
rimraf.sync(appDir);
|
|
204
|
+
rimraf.sync(`${appDir}${BACKUP_EXT}`);
|
|
205
|
+
logger.info('Uninstalled', projectId);
|
|
206
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { keyMirrors, ModelInstallPayload } from '@alwaysai/device-agent-schemas';
|
|
2
|
+
import { app } from 'alwaysai/lib/components';
|
|
3
|
+
import {
|
|
4
|
+
appModelsAddComponent,
|
|
5
|
+
appModelsRemoveAllComponent,
|
|
6
|
+
appModelsRemoveComponent,
|
|
7
|
+
appModelsUpdateComponent,
|
|
8
|
+
} from 'alwaysai/lib/components/app';
|
|
9
|
+
import { JsSpawner } from 'alwaysai/lib/util';
|
|
10
|
+
import { logger } from '../util/logger';
|
|
11
|
+
import { existsSync, createReadStream } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
14
|
+
import { copyDir } from '../util/copy-dir';
|
|
15
|
+
import { runInDir } from '../util/run-in-dir';
|
|
16
|
+
import { getAppStatus, restartApp } from './status';
|
|
17
|
+
|
|
18
|
+
import { ModelDetails } from './types';
|
|
19
|
+
import {
|
|
20
|
+
buildApp,
|
|
21
|
+
downloadPackageUsingPresignedUrl,
|
|
22
|
+
getAppDir,
|
|
23
|
+
requireAppInstalled,
|
|
24
|
+
} from './utils';
|
|
25
|
+
|
|
26
|
+
export async function getAppModels(props: { projectId: string }) {
|
|
27
|
+
const { projectId } = props;
|
|
28
|
+
if (!(await AgentConfigFile().isAppPresent({ projectId }))) {
|
|
29
|
+
throw new Error('Application is not installed');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const appDir = getAppDir(projectId);
|
|
33
|
+
const appCfgPath = join(appDir, 'alwaysai.app.json');
|
|
34
|
+
if (!existsSync(appCfgPath)) {
|
|
35
|
+
throw new Error('Application config not found!');
|
|
36
|
+
}
|
|
37
|
+
const appCfg = JSON.parse(await JsSpawner().readFile(appCfgPath));
|
|
38
|
+
const modelDetails: ModelDetails[] = [];
|
|
39
|
+
if (!('models' in appCfg)) {
|
|
40
|
+
return modelDetails;
|
|
41
|
+
}
|
|
42
|
+
for (const model in appCfg['models']) {
|
|
43
|
+
modelDetails.push({ modelId: model, version: appCfg['models'][model] });
|
|
44
|
+
}
|
|
45
|
+
return modelDetails;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function addModel(props: { projectId: string; modelId: string }) {
|
|
49
|
+
const { projectId, modelId } = props;
|
|
50
|
+
await requireAppInstalled({ projectId });
|
|
51
|
+
|
|
52
|
+
const appDir = getAppDir(projectId);
|
|
53
|
+
await appModelsAddComponent({
|
|
54
|
+
yes: false,
|
|
55
|
+
dir: appDir,
|
|
56
|
+
id: modelId,
|
|
57
|
+
addToProject: false,
|
|
58
|
+
});
|
|
59
|
+
await buildApp({ appDir });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function removeModel(props: { projectId: string; modelId: string }) {
|
|
63
|
+
const { projectId, modelId } = props;
|
|
64
|
+
await requireAppInstalled({ projectId });
|
|
65
|
+
|
|
66
|
+
const appDir = getAppDir(projectId);
|
|
67
|
+
|
|
68
|
+
await runInDir(
|
|
69
|
+
appModelsRemoveComponent,
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
id: modelId,
|
|
73
|
+
purge: true,
|
|
74
|
+
removeFromProject: false,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
appDir,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function replaceModels(props: { projectId: string; modelIds: string[] }) {
|
|
82
|
+
const { projectId, modelIds } = props;
|
|
83
|
+
await requireAppInstalled({ projectId });
|
|
84
|
+
|
|
85
|
+
const appDir = getAppDir(projectId);
|
|
86
|
+
|
|
87
|
+
await runInDir(
|
|
88
|
+
appModelsRemoveAllComponent,
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
purge: true,
|
|
92
|
+
removeFromProject: false,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
appDir,
|
|
96
|
+
);
|
|
97
|
+
for (const modelId of modelIds) {
|
|
98
|
+
await appModelsAddComponent({
|
|
99
|
+
yes: false,
|
|
100
|
+
dir: appDir,
|
|
101
|
+
id: modelId,
|
|
102
|
+
addToProject: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
await buildApp({ appDir });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function updateModels(props: { projectId: string }) {
|
|
109
|
+
const { projectId } = props;
|
|
110
|
+
await requireAppInstalled({ projectId });
|
|
111
|
+
|
|
112
|
+
const appDir = getAppDir(projectId);
|
|
113
|
+
await appModelsUpdateComponent({
|
|
114
|
+
yes: false,
|
|
115
|
+
dir: appDir,
|
|
116
|
+
});
|
|
117
|
+
await buildApp({ appDir });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function installModelsWithPresignedURLs(
|
|
121
|
+
modelPayloads: ModelInstallPayload[],
|
|
122
|
+
targetDir: string,
|
|
123
|
+
) {
|
|
124
|
+
const spawner = JsSpawner();
|
|
125
|
+
await Promise.all(
|
|
126
|
+
modelPayloads.map(async (payload: ModelInstallPayload) => {
|
|
127
|
+
const modelDest = `${targetDir}/${payload.id}`;
|
|
128
|
+
await spawner.mkdirp(modelDest);
|
|
129
|
+
const localDest = `${modelDest}/${payload.version}.tar.gz`;
|
|
130
|
+
await downloadPackageUsingPresignedUrl({
|
|
131
|
+
localDest,
|
|
132
|
+
presignedUrl: payload.modelSignedUrl,
|
|
133
|
+
});
|
|
134
|
+
await spawner.untar(createReadStream(localDest), dirname(modelDest));
|
|
135
|
+
await spawner.rimraf(localDest);
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function updateModelsWithPresignedUrls(
|
|
141
|
+
project: string,
|
|
142
|
+
modelInstallPayloads: ModelInstallPayload[],
|
|
143
|
+
) {
|
|
144
|
+
// create temp dir with all untouched models
|
|
145
|
+
const spawner = JsSpawner();
|
|
146
|
+
const appDir = getAppDir(project);
|
|
147
|
+
const ogDir = `${appDir}/models`;
|
|
148
|
+
const tmpDir = `${ogDir}.tmp`;
|
|
149
|
+
const restoreDir = `${ogDir}.restore`;
|
|
150
|
+
await copyDir({ srcPath: ogDir, destPath: restoreDir });
|
|
151
|
+
await spawner.rimraf(tmpDir);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Move unchanged models
|
|
155
|
+
const existingModels = await getAppModels({ projectId: project });
|
|
156
|
+
for (const model of existingModels) {
|
|
157
|
+
if (
|
|
158
|
+
!modelInstallPayloads
|
|
159
|
+
.map((newModel: ModelInstallPayload) => newModel.id)
|
|
160
|
+
.includes(model.modelId)
|
|
161
|
+
) {
|
|
162
|
+
// model does not need to be updated
|
|
163
|
+
await copyDir({
|
|
164
|
+
srcPath: `${ogDir}/${model.modelId}`,
|
|
165
|
+
destPath: `${tmpDir}/${model.modelId}`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// install model packages
|
|
171
|
+
await installModelsWithPresignedURLs(modelInstallPayloads, tmpDir);
|
|
172
|
+
await spawner.rimraf(ogDir);
|
|
173
|
+
await copyDir({ srcPath: tmpDir, destPath: ogDir });
|
|
174
|
+
await spawner.rimraf(tmpDir);
|
|
175
|
+
|
|
176
|
+
await buildApp({ appDir: getAppDir(project) });
|
|
177
|
+
|
|
178
|
+
const appStatus = await getAppStatus({ projectId: project });
|
|
179
|
+
const appIsRunning =
|
|
180
|
+
appStatus.services.length &&
|
|
181
|
+
appStatus.services[0].state !== keyMirrors.appState.stopped;
|
|
182
|
+
|
|
183
|
+
if (appIsRunning) {
|
|
184
|
+
restartApp({ projectId: project });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
logger.info(`Models installed for project ${project}`);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
logger.error(e, 'Error updating app models from presigned URL, restoring models.');
|
|
190
|
+
await spawner.rimraf(tmpDir);
|
|
191
|
+
await spawner.rimraf(ogDir);
|
|
192
|
+
await copyDir({ srcPath: restoreDir, destPath: ogDir });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import compose from 'docker-compose';
|
|
2
|
+
import { fetchAppReleaseHistory } from 'alwaysai/lib/infrastructure';
|
|
3
|
+
import { JsSpawner } from 'alwaysai/lib/util';
|
|
4
|
+
|
|
5
|
+
import { runDockerLogin } from '../docker/docker-cmd';
|
|
6
|
+
import { getAppDir, requireAppInstalled } from './utils';
|
|
7
|
+
import {
|
|
8
|
+
ServiceStatusPacket,
|
|
9
|
+
AppStatePacket,
|
|
10
|
+
keyMirrors,
|
|
11
|
+
AppStateValue,
|
|
12
|
+
} from '@alwaysai/device-agent-schemas';
|
|
13
|
+
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
14
|
+
import { logger } from '../util/logger';
|
|
15
|
+
|
|
16
|
+
export async function listAppReleases(props: { projectId: string }) {
|
|
17
|
+
const { projectId } = props;
|
|
18
|
+
const releaseHistory = await fetchAppReleaseHistory(projectId);
|
|
19
|
+
return releaseHistory;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function listAppLatestRelease(props: { projectId: string }) {
|
|
23
|
+
const { projectId } = props;
|
|
24
|
+
const releaseHistory = await fetchAppReleaseHistory(projectId);
|
|
25
|
+
if (releaseHistory.length >= 1) {
|
|
26
|
+
return releaseHistory[0]['releaseHash'];
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getAppStatus(props: {
|
|
32
|
+
projectId: string;
|
|
33
|
+
}): Promise<AppStatePacket> {
|
|
34
|
+
const { projectId } = props;
|
|
35
|
+
if (!(await AgentConfigFile().isAppPresent({ projectId }))) {
|
|
36
|
+
throw new Error('Application is not installed');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const appDetails = {
|
|
40
|
+
projectId,
|
|
41
|
+
version: await AgentConfigFile().getAppVersion({ projectId }),
|
|
42
|
+
};
|
|
43
|
+
if (!(await AgentConfigFile().isAppReady({ projectId }))) {
|
|
44
|
+
// App is being installed or updated
|
|
45
|
+
return { appDetails, services: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const appDir = getAppDir(projectId);
|
|
49
|
+
const composeServices = await compose.configServices({ cwd: appDir });
|
|
50
|
+
|
|
51
|
+
// Check if app is running
|
|
52
|
+
const status = await compose.ps({ cwd: appDir });
|
|
53
|
+
|
|
54
|
+
if (status.exitCode !== 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Failed to get application status! stdout=${status.out} stderr=${status.err}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const services: ServiceStatusPacket[] = [];
|
|
61
|
+
const spawner = JsSpawner({ path: appDir });
|
|
62
|
+
for (const name of composeServices.data.services) {
|
|
63
|
+
// Get container name for service
|
|
64
|
+
const containerId = await spawner.run({
|
|
65
|
+
exe: 'docker-compose',
|
|
66
|
+
cwd: appDir,
|
|
67
|
+
args: ['ps', '-q', name],
|
|
68
|
+
});
|
|
69
|
+
if (containerId === '') {
|
|
70
|
+
// The service was not yet started or failed to start
|
|
71
|
+
services.push({ name, state: keyMirrors.appState.stopped });
|
|
72
|
+
} else {
|
|
73
|
+
const slashContainerName = await spawner.run({
|
|
74
|
+
exe: 'docker',
|
|
75
|
+
args: ['inspect', '-f', '{{.Name}}', containerId],
|
|
76
|
+
});
|
|
77
|
+
const containerName = slashContainerName.substring(1);
|
|
78
|
+
|
|
79
|
+
let foundContainer = false;
|
|
80
|
+
for (const container of status.data.services) {
|
|
81
|
+
if (container.name !== containerName) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
let state: AppStateValue;
|
|
85
|
+
if (container.state.includes('Up')) {
|
|
86
|
+
state = keyMirrors.appState.up;
|
|
87
|
+
} else if (
|
|
88
|
+
container.state.includes('Stopped') ||
|
|
89
|
+
container.state.includes('Exit')
|
|
90
|
+
) {
|
|
91
|
+
state = keyMirrors.appState.stopped;
|
|
92
|
+
} else if (container.state.includes('Restarting')) {
|
|
93
|
+
state = keyMirrors.appState.restarting;
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error(`Unknown state for service! (${container.state})`);
|
|
96
|
+
}
|
|
97
|
+
services.push({ name, state });
|
|
98
|
+
foundContainer = true;
|
|
99
|
+
}
|
|
100
|
+
if (!foundContainer) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Unable to find container for ${name} in ${JSON.stringify(
|
|
103
|
+
status.data.services,
|
|
104
|
+
null,
|
|
105
|
+
2,
|
|
106
|
+
)}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { appDetails, services };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getAppLogs(props: {
|
|
115
|
+
projectId: string;
|
|
116
|
+
services?: string[];
|
|
117
|
+
args?: string[];
|
|
118
|
+
}): Promise<NodeJS.ReadableStream> {
|
|
119
|
+
const { projectId, services, args } = props;
|
|
120
|
+
await requireAppInstalled({ projectId });
|
|
121
|
+
|
|
122
|
+
const appDir = getAppDir(projectId);
|
|
123
|
+
|
|
124
|
+
const serviceList = services
|
|
125
|
+
? services
|
|
126
|
+
: await (async function () {
|
|
127
|
+
const composeServices = await compose.configServices({ cwd: appDir });
|
|
128
|
+
return composeServices.data.services;
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
const argsList = args ? args : [];
|
|
132
|
+
|
|
133
|
+
// Use direct command with spawner in order to get a readable stream
|
|
134
|
+
return await JsSpawner().runStreaming({
|
|
135
|
+
exe: 'docker-compose',
|
|
136
|
+
args: ['logs', '-f', ...argsList, ...serviceList],
|
|
137
|
+
cwd: appDir,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function startApp(props: {
|
|
142
|
+
projectId: string;
|
|
143
|
+
dockerLoginToken?: string;
|
|
144
|
+
}): Promise<void> {
|
|
145
|
+
const { projectId, dockerLoginToken } = props;
|
|
146
|
+
await requireAppInstalled({ projectId });
|
|
147
|
+
|
|
148
|
+
const appDir = getAppDir(projectId);
|
|
149
|
+
if (dockerLoginToken !== undefined) {
|
|
150
|
+
const result = await runDockerLogin({ token: dockerLoginToken });
|
|
151
|
+
logger.info(result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// TODO: Check if app is running
|
|
155
|
+
// Start app
|
|
156
|
+
const upOut = await compose.upAll({ cwd: appDir });
|
|
157
|
+
logger.info(upOut);
|
|
158
|
+
if (upOut.exitCode !== 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Failed to start application! stdout=${upOut.out} stderr=${upOut.err}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
logger.info('Started', projectId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function stopApp(props: { projectId: string }): Promise<void> {
|
|
167
|
+
const { projectId } = props;
|
|
168
|
+
await requireAppInstalled({ projectId });
|
|
169
|
+
|
|
170
|
+
const appDir = getAppDir(projectId);
|
|
171
|
+
// TODO: Check if app is running
|
|
172
|
+
// Stop app
|
|
173
|
+
const output = await compose.down({ cwd: appDir });
|
|
174
|
+
logger.info(output);
|
|
175
|
+
if (output.exitCode !== 0) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Failed to stop application! stdout=${output.out} stderr=${output.err}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
logger.info('Stopped', projectId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function restartApp(props: { projectId: string }): Promise<void> {
|
|
184
|
+
const { projectId } = props;
|
|
185
|
+
await stopApp({ projectId });
|
|
186
|
+
await startApp({ projectId });
|
|
187
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ModelDetails = { modelId: string; version: number };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import compose from 'docker-compose';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { AAI_DIR, TARGET_JSON_FILE_NAME } from 'alwaysai/lib/constants';
|
|
5
|
+
|
|
6
|
+
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
7
|
+
import nodeFetch from 'node-fetch';
|
|
8
|
+
import { TargetJsonFile } from 'alwaysai/lib/core/app';
|
|
9
|
+
import { appDeployLinuxAndRemoteDevice } from 'alwaysai/lib/components/app';
|
|
10
|
+
import { runInDir } from '../util/run-in-dir';
|
|
11
|
+
import { JsSpawner } from 'alwaysai/lib/util';
|
|
12
|
+
import { logger } from '../util/logger';
|
|
13
|
+
|
|
14
|
+
export const APP_ROOT = path.join(AAI_DIR, 'applications');
|
|
15
|
+
|
|
16
|
+
export function getAppDir(projectId: string): string {
|
|
17
|
+
return path.join(APP_ROOT, projectId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function requireAppInstalled(props: { projectId: string }) {
|
|
21
|
+
const { projectId } = props;
|
|
22
|
+
// Ensure an app that is being installed or updated is not modified
|
|
23
|
+
if (!(await AgentConfigFile().isAppPresent({ projectId }))) {
|
|
24
|
+
throw new Error('Application is not installed');
|
|
25
|
+
}
|
|
26
|
+
if (!(await AgentConfigFile().isAppReady({ projectId }))) {
|
|
27
|
+
throw new Error('Application is not done installing or updating');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function buildApp(props: { appDir: string }) {
|
|
32
|
+
const { appDir } = props;
|
|
33
|
+
|
|
34
|
+
// Build standalone image and docker-compose
|
|
35
|
+
const targetJson = TargetJsonFile(appDir);
|
|
36
|
+
const targetConfig = targetJson.read();
|
|
37
|
+
if (targetConfig.targetProtocol !== 'docker:') {
|
|
38
|
+
throw new Error(`${TARGET_JSON_FILE_NAME} is not properly configured!`);
|
|
39
|
+
}
|
|
40
|
+
await runInDir(
|
|
41
|
+
appDeployLinuxAndRemoteDevice,
|
|
42
|
+
[
|
|
43
|
+
{
|
|
44
|
+
yes: true,
|
|
45
|
+
generateDockerCompose: true,
|
|
46
|
+
logs: false,
|
|
47
|
+
stop: false,
|
|
48
|
+
targetJson,
|
|
49
|
+
targetConfig,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
appDir,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const buildOut = await compose.buildAll({ cwd: appDir });
|
|
56
|
+
logger.info(buildOut);
|
|
57
|
+
if (buildOut.exitCode !== 0) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Failed to build application! stdout=${buildOut.out} stderr=${buildOut.err}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function downloadPackageUsingPresignedUrl(props: {
|
|
65
|
+
localDest: string;
|
|
66
|
+
presignedUrl: string;
|
|
67
|
+
}): Promise<void> {
|
|
68
|
+
logger.info('downloading URL');
|
|
69
|
+
const { localDest, presignedUrl } = props;
|
|
70
|
+
const response = await nodeFetch(presignedUrl);
|
|
71
|
+
if (response.status !== 200) {
|
|
72
|
+
// If the URL is invalid; I think we shouldn't get here with the new changes
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Status Code: ${response.status}, ${response.statusText}. Response: ${response.body}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Write the app package to the hash directory
|
|
80
|
+
*/
|
|
81
|
+
const stream = response.body.pipe(fs.createWriteStream(localDest));
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
stream.on('finish', resolve);
|
|
84
|
+
stream.on('error', reject);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function getAppConfig(project: string) {
|
|
89
|
+
const appCfgPath = path.join(getAppDir(project), 'alwaysai.app.json');
|
|
90
|
+
if (!fs.existsSync(appCfgPath)) {
|
|
91
|
+
throw new Error(`Application config not found for project ${project}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return JSON.parse(await JsSpawner().readFile(appCfgPath));
|
|
95
|
+
}
|