@codiac.io/codiac-cli 1.3.139 → 1.3.141

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.
@@ -1,604 +1,388 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RelayContainer = exports.DEFAULT_IMAGE_TAG = void 0;
3
+ exports.DEFAULT_IMAGE_TAG = exports.RelayContainer = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const rxjs_1 = require("rxjs");
6
6
  const operators_1 = require("rxjs/operators");
7
7
  const reqp = require("request-promise");
8
8
  const os = require("os");
9
- const fs = require("fs");
9
+ const fs = require("fs/promises");
10
10
  const path = require("path");
11
- const child_process_1 = require("child_process");
11
+ const Nats = require("@nats-io/nats-core");
12
+ const Transport = require("@nats-io/transport-node");
12
13
  const inversify_1 = require("inversify");
13
- const codiac_error_1 = require("@codiac.io/codiac-common/contracts/codiac-error");
14
- const codiac_common_1 = require("@codiac.io/codiac-common");
15
- const IBetterLogger_1 = require("@codiac.io/codiac-common/IBetterLogger");
16
- const cli_tools_1 = require("@codiac.io/cli-tools");
17
- const INTERFACES_CLI_1 = require("../ops/INTERFACES_CLI");
18
14
  const Docker = require("dockerode");
15
+ const codiac_common_1 = require("@codiac.io/codiac-common");
16
+ const ops_1 = require("../ops");
19
17
  const entities_1 = require("../ops/entities");
20
- const DEFAULT_IMAGE_NAME = "codiacimages/codiac-relay";
21
- exports.DEFAULT_IMAGE_TAG = "1.6.59";
22
- const DEFAULT_CONTAINER_NAME = "codiac-relay";
23
- const DEFAULT_HEALTH_CHECK_URL = "http://localhost:5799/heartbeat-json"; // /ready-state";
24
- const DEFAULT_RETRY_TIMOUT_IN_MINUTES = 1;
18
+ // Configuration defaults
19
+ const DEFAULT_CONFIG = {
20
+ IMAGE_NAME: "codiacimages/codiac-relay",
21
+ IMAGE_TAG: "1.6.59",
22
+ CONTAINER_NAME: "codiac-relay",
23
+ HEALTH_CHECK_URL: "http://localhost:5799/heartbeat-json",
24
+ RETRY_TIMEOUT_MINUTES: 1,
25
+ MAX_HEALTH_CHECK_RETRIES: 40,
26
+ HEALTH_CHECK_INTERVAL_MS: 3000,
27
+ EXPOSED_PORTS: {
28
+ "5799/tcp": {},
29
+ "5798/tcp": {},
30
+ "4222/tcp": {},
31
+ "9222/tcp": {},
32
+ },
33
+ PORT_BINDINGS: {
34
+ "5799/tcp": [{ HostPort: "5799" }],
35
+ "5798/tcp": [{ HostPort: "5798" }],
36
+ "4222/tcp": [{ HostPort: "4222" }],
37
+ "9222/tcp": [{ HostPort: "9222" }],
38
+ },
39
+ VOLUMES: { "/root/.codiac": {} },
40
+ BINDS: [`${os.homedir()}/.codiac:/root/.codiac`],
41
+ };
42
+ /**
43
+ * Manages a single version of a Codiac Relay Docker container, ensuring only the specified version runs.
44
+ */
25
45
  let RelayContainer = class RelayContainer {
26
46
  get userMessages() {
27
- return this._userMessages.asObservable();
28
- }
29
- get dockerFailed$() {
30
- return this.dockerFailedSubj.asObservable();
31
- }
32
- get docker$() {
33
- return this.dockerSubj.asObservable();
34
- }
35
- get startFailed$() {
36
- return this.startFailedSubj.asObservable();
47
+ return this.userMessagesSubj.asObservable();
37
48
  }
38
49
  get ready$() {
39
50
  return this.readySubj.pipe((0, operators_1.distinctUntilChanged)());
40
51
  }
41
- constructor(styler, cliEnviro, logger, healthCheckUrl = DEFAULT_HEALTH_CHECK_URL, retryTimeOut = DEFAULT_RETRY_TIMOUT_IN_MINUTES, projectRoot, localEntities = false) {
52
+ constructor(styler, cliEnviro, logger, healthCheckUrl = DEFAULT_CONFIG.HEALTH_CHECK_URL, retryTimeOut = DEFAULT_CONFIG.RETRY_TIMEOUT_MINUTES, projectRoot, localEntities = false) {
42
53
  this.styler = styler;
43
54
  this.cliEnviro = cliEnviro;
44
55
  this.logger = logger;
45
- this.healthCheckUrl = healthCheckUrl;
46
- this.retryTimeOut = retryTimeOut;
47
56
  this.projectRoot = projectRoot;
48
57
  this.localEntities = localEntities;
49
- this.imageName = DEFAULT_IMAGE_NAME;
50
- this.imageTag = exports.DEFAULT_IMAGE_TAG;
51
- this.containerName = DEFAULT_CONTAINER_NAME;
58
+ this.natsClient = new rxjs_1.BehaviorSubject(null);
59
+ this.destroySubj = new rxjs_1.Subject();
60
+ this.userMessagesSubj = new rxjs_1.BehaviorSubject(undefined);
61
+ this.readySubj = new rxjs_1.ReplaySubject(1);
62
+ this.readyFailedSubj = new rxjs_1.Subject();
63
+ this.connectionRefusedCount = new rxjs_1.BehaviorSubject(0);
52
64
  this.silent = false;
53
- this.readyFailed$ = new rxjs_1.Subject();
54
- this.connectionRefusedCount = 0;
55
- this.serverErrorCount = 0;
56
- this.MAXTRIESBEFORECONTAINERRESTART = 40; // Equivalent to over 40 seconds for the container to get healthy
57
- this.isWin = process.platform === "win32";
58
- // https://github.com/apocas/dockerode/issues/290
59
- this.socketPath = this.isWin
60
- ? "//./pipe/docker_engine"
61
- : undefined;
62
- this._userMessages = new rxjs_1.BehaviorSubject(undefined);
63
- // private startSubj: Subject<string> = new Subject<string>();
64
- // private get start$(): Observable<string> {
65
- // return this.startSubj.asObservable();
66
- // }
67
- this.dockerFailedSubj = new rxjs_1.Subject();
68
- this.dockerSubj = new rxjs_1.Subject();
69
- // started is based on the container we're trying to start
70
- this.startFailedSubj = new rxjs_1.Subject();
71
- this.readySubj = new rxjs_1.ReplaySubject();
72
- this.promisifyStream = (stream) => new Promise((resolve, reject) => {
73
- stream.on('data', (d) => this.logger.write(d.toString()));
74
- stream.on('end', resolve);
75
- stream.on('error', reject);
76
- });
77
- this._cliExec = new cli_tools_1.CliExec(logger);
78
- this.docker = new Docker();
79
- this.init();
65
+ this.config = Object.assign(Object.assign({}, DEFAULT_CONFIG), { HEALTH_CHECK_URL: healthCheckUrl, RETRY_TIMEOUT_MINUTES: retryTimeOut });
66
+ this.docker = this.instantiateDocker();
67
+ this.checkDockerAvailability();
80
68
  }
81
- /** Creates subfolders in codiac program folder required for local entities.
82
- *
83
- * Does not throw: Logs any encountered errors and continues.
84
- */
85
- createLocalDataFolders() {
86
- var _a;
87
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
88
- const folders = [
89
- "cluster-images",
90
- "templates",
91
- "data/db",
92
- "data/logs"
93
- ];
94
- for (let folder of folders) {
95
- try {
96
- const fqn = path.posix.join(this.cliEnviro.localProgramFolder, folder);
97
- if (!fs.existsSync(fqn)) {
98
- yield fs.promises.mkdir(fqn, { recursive: true });
99
- this.logger.debug("Created local data folder ".concat("[", fqn, "]."));
100
- }
101
- }
102
- catch (err) {
103
- this.logger.error("Error creating local data folder ".concat("[", folder, "]: ", (_a = err.message) !== null && _a !== void 0 ? _a : "(no message available)"));
104
- }
105
- }
106
- return;
107
- });
69
+ instantiateDocker() {
70
+ return new Docker(process.platform === "win32" ? { socketPath: "//./pipe/docker_engine" } : {});
108
71
  }
109
72
  init() {
110
- //Check if docker is running stream to dockerSubj or dockerFailedSubj
111
- this.getDocker();
112
- // This should hit anytime the image tag changes, which triggers the workflow to rip and replace if its not the same versions
113
- (0, rxjs_1.of)(this.imageTag)
114
- .pipe((0, operators_1.distinctUntilChanged)()) // i think to be able to change image dynamiclly
115
- .pipe((0, operators_1.concatMap)(_ => this.versionMismatch())) //check the version mismatch or no version do some work
116
- .pipe((0, operators_1.concatMap)(versionMismatch => {
117
- if (versionMismatch) {
118
- this._userMessages.next(`Version mismatch of the Relay container found. Updating to ${this.imageTag}`);
119
- return this.stop()
120
- .pipe((0, operators_1.concatMap)(_ => this.remove()))
121
- .pipe((0, operators_1.concatMap)(_ => this.deleteRelayImage()))
122
- .pipe((0, operators_1.concatMap)(_ => this.pullImage()));
123
- }
124
- else {
125
- // The api's may already be running. Let's see.
126
- return (0, rxjs_1.of)("no work was done");
127
- }
128
- }))
129
- .subscribe(_ => this.healthCheck());
130
- // If we find docker is not started, we put ourselves into a loop, asking the user to kick it on
131
- this.dockerFailed$
132
- .pipe((0, operators_1.delay)(10000))
133
- .pipe((0, operators_1.tap)(_ => {
134
- this.logger.debug("Attempting to get service again");
135
- this.getDocker();
136
- }))
137
- .subscribe();
138
- // If docker is running but the api's are not, let's try to start the container optimistically
139
- (0, rxjs_1.combineLatest)([this.docker$, this.readyFailed$])
140
- .pipe((0, operators_1.tap)(docker => {
141
- this.execStart();
142
- }))
143
- .pipe((0, operators_1.takeUntil)(this.ready$))
73
+ (0, rxjs_1.of)(`${this.config.IMAGE_NAME}:${this.config.IMAGE_TAG}`)
74
+ .pipe((0, operators_1.distinctUntilChanged)(), (0, operators_1.concatMap)(() => this.handleVersionMismatch()), (0, operators_1.takeUntil)(this.destroySubj))
144
75
  .subscribe();
145
- // If docker is running but the conainer failed to start. Kick on CONTAINER from image
146
- (0, rxjs_1.combineLatest)([this.docker$, this.startFailed$])
147
- .pipe((0, operators_1.tap)(([docker, containerName]) => {
148
- this.createContainer();
149
- }))
150
- .pipe((0, operators_1.takeUntil)(this.ready$))
76
+ this.readyFailedSubj
77
+ .pipe((0, operators_1.tap)(() => this.logNotice(this.styler.hilite("Starting Services..."))), (0, operators_1.concatMap)(() => (0, rxjs_1.interval)(this.config.HEALTH_CHECK_INTERVAL_MS)
78
+ .pipe((0, operators_1.timeout)(this.config.RETRY_TIMEOUT_MINUTES * 60000), (0, operators_1.take)(this.config.MAX_HEALTH_CHECK_RETRIES), (0, operators_1.takeUntil)(this.ready$), (0, operators_1.takeUntil)(this.destroySubj), (0, operators_1.concatMap)(() => this.performHealthCheck()))))
151
79
  .subscribe();
152
- // // Now our pipelines are defined and hot. We fire off the catalyst and notify user.
153
- this.readyFailed$
154
- .pipe((0, operators_1.take)(1))
155
- .pipe((0, operators_1.tap)(() => {
156
- this.logger.notice(this.styler.hilite(`Starting Services...`));
157
- }))
80
+ }
81
+ logDebug(message) {
82
+ this.logger.debug(message);
83
+ }
84
+ logNotice(message) {
85
+ this.logger.notice(message);
86
+ }
87
+ createLocalDataFolders() {
88
+ if (!this.localEntities)
89
+ return (0, rxjs_1.of)(true);
90
+ const folders = ["cluster-images", "templates", "data/db", "data/logs"];
91
+ return (0, rxjs_1.from)(Promise.all(folders.map((folder) => fs.mkdir(path.posix.join(this.cliEnviro.localProgramFolder, folder), { recursive: true })
92
+ .then(() => this.logDebug(`Created local data folder [${folder}]`))
93
+ .catch((err) => {
94
+ this.logger.error(`Error creating local data folder [${folder}]: ${err.message}`);
95
+ throw err;
96
+ })))).pipe((0, operators_1.map)(() => true), (0, operators_1.catchError)(() => (0, rxjs_1.of)(false)));
97
+ }
98
+ checkDockerAvailability() {
99
+ (0, rxjs_1.from)(this.docker.info())
158
100
  .pipe((0, operators_1.tap)(() => {
159
- // every three seconds we're going to health check until either ready or timeout
160
- (0, rxjs_1.interval)(3000)
161
- .pipe((0, operators_1.take)(100))
162
- .pipe((0, operators_1.timeout)(this.retryTimeOut * 60000)) //minutes to miliseconds
163
- .pipe((0, operators_1.takeUntil)(this.ready$))
164
- .subscribe(() => this.healthCheck());
165
- }))
101
+ this.logDebug("Docker daemon is available");
102
+ this.init();
103
+ }), (0, operators_1.catchError)((err) => {
104
+ const msg = "Can you start Docker for me?";
105
+ this.logger.error(msg, err);
106
+ this.userMessagesSubj.next(msg);
107
+ return (0, rxjs_1.of)(null).pipe((0, operators_1.delay)(6000), (0, operators_1.tap)(() => {
108
+ this.docker = this.instantiateDocker();
109
+ this.checkDockerAvailability();
110
+ }));
111
+ }), (0, operators_1.takeUntil)(this.destroySubj))
166
112
  .subscribe();
167
113
  }
168
- getAllContainersOfRelayImage() {
169
- const command = `docker ps -a | grep -v ID`;
170
- const output = new rxjs_1.Subject();
171
- this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
172
- .then((stdout) => {
173
- this.logger.debug(`getRelayImageFromDockerPS - entered method`);
174
- // Split the output into lines and parse each line as JSON
175
- try {
176
- if (stdout == undefined && stdout == "")
177
- output.next(undefined);
178
- else {
179
- const listedContainers = stdout.split("\n");
180
- let containerIds = [];
181
- listedContainers.forEach(r => {
182
- if (r.split(" ")[1].includes(this.imageName))
183
- containerIds.push(r.split(" ")[0]);
184
- });
185
- output.next(containerIds);
186
- }
187
- }
188
- catch (parseError) {
189
- this.logger.error(`getRelayImageFromDockerPS - Error parsing Docker output: ${parseError}`);
190
- return parseError;
191
- }
192
- })
193
- .catch(error => {
194
- this.logger.debug(`getRelayImageFromDockerPS - Command failed: docker ps -a | grep -v ID`);
195
- output.next(undefined);
196
- });
197
- return output.asObservable();
114
+ getRunningContainers() {
115
+ return (0, rxjs_1.from)(this.docker.listContainers({ all: true })).pipe((0, operators_1.map)((containers) => containers.filter((c) => c.Image.includes(this.config.IMAGE_NAME) && c.State === "running")), (0, operators_1.concatMap)((matching) => matching.length === 0
116
+ ? (0, rxjs_1.of)([])
117
+ : (0, rxjs_1.from)(Promise.all(matching.map((c) => this.docker.getContainer(c.Id).inspect())))), (0, operators_1.catchError)((err) => {
118
+ this.logger.error(`Failed to list containers: ${err.message}`);
119
+ return (0, rxjs_1.of)([]);
120
+ }));
198
121
  }
199
- getRelayImageFromDockerPS() {
200
- const command = `docker ps -a | grep -v ID`;
201
- const output = new rxjs_1.Subject();
202
- this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
203
- .then((stdout) => {
204
- this.logger.debug(`getRelayImageFromDockerPS - entered method`);
205
- // Split the output into lines and parse each line as JSON
206
- try {
207
- if (stdout == undefined && stdout == "")
208
- output.next(undefined);
209
- else {
210
- const listedContainers = stdout.split("\n");
211
- const relayImangeName = listedContainers.find(r => {
212
- const name = r.split(" ")[1].includes(this.imageName);
213
- if (name)
214
- return name;
215
- });
216
- if (relayImangeName != undefined && relayImangeName != "") {
217
- output.next(relayImangeName.split(" ")[1]);
218
- }
219
- else
220
- output.next(undefined);
221
- }
122
+ handleVersionMismatch() {
123
+ const imageRef = `${this.config.IMAGE_NAME}:${this.config.IMAGE_TAG}`;
124
+ return this.getRunningContainers().pipe((0, operators_1.concatMap)((containers) => {
125
+ if (containers.length === 0) {
126
+ this.logDebug("No running containers found");
127
+ return this.startContainer();
222
128
  }
223
- catch (parseError) {
224
- this.logger.error(`getRelayImageFromDockerPS - Error parsing Docker output: ${parseError}`);
225
- return parseError;
226
- }
227
- })
228
- .catch(error => {
229
- this.logger.debug(`getRelayImageFromDockerPS - Command failed: docker ps -a | grep -v ID`);
230
- output.next(undefined);
231
- });
232
- return output.asObservable();
129
+ const mismatchChecks = containers.map((c) => {
130
+ const imageName = c.Image;
131
+ if (imageName.startsWith("sha256:")) {
132
+ return (0, rxjs_1.from)(this.docker.getImage(imageName).inspect()).pipe((0, operators_1.map)((info) => !(info.RepoTags || []).includes(imageRef)), (0, operators_1.catchError)((err) => {
133
+ this.logger.error(`Failed to inspect image ${imageName}: ${err.message}`);
134
+ return (0, rxjs_1.of)(true);
135
+ }));
136
+ }
137
+ return (0, rxjs_1.of)(!imageName.includes(this.config.IMAGE_TAG));
138
+ });
139
+ return (0, rxjs_1.forkJoin)(mismatchChecks).pipe((0, operators_1.map)((results) => results.some((hasMismatch) => hasMismatch)), (0, operators_1.concatMap)((hasMismatch) => {
140
+ if (!hasMismatch) {
141
+ this.logDebug(`Container version matches ${imageRef}`);
142
+ return this.performHealthCheck();
143
+ }
144
+ this.userMessagesSubj.next(`Version mismatch detected. Updating to ${imageRef}`);
145
+ return this.stopAndRemoveContainers().pipe((0, operators_1.concatMap)(() => this.deleteMismatchedImages()), (0, operators_1.concatMap)(() => this.pullImage()), (0, operators_1.concatMap)(() => this.startContainer()));
146
+ }));
147
+ }));
233
148
  }
234
- getRunningContainers() {
235
- const output = new rxjs_1.Subject();
236
- return this.getRelayImageFromDockerPS()
237
- .pipe((0, operators_1.concatMap)(running => {
238
- if (running) {
239
- const command = `docker inspect ${running}`;
240
- this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
241
- .then((stdout) => {
242
- // Split the output into lines and parse each line as JSON
243
- try {
244
- if (stdout != undefined && stdout != "") {
245
- const images = JSON.parse(stdout);
246
- output.next(images);
247
- }
248
- else
249
- output.next(undefined);
250
- }
251
- catch (parseError) {
252
- this.logger.error(`Error getting running containers: ${parseError}`);
253
- output.next(undefined);
254
- }
255
- })
256
- .catch(error => {
257
- this.logger.debug(`getRunningContainers - ${error.message}`);
258
- output.next(undefined);
259
- });
260
- }
261
- else {
262
- this.logger.debug(`getRunningContainers - No running containers found.`);
263
- return (0, rxjs_1.of)(undefined);
149
+ stopAndRemoveContainers() {
150
+ return this.getRunningContainers().pipe((0, operators_1.concatMap)((containers) => {
151
+ if (containers.length === 0) {
152
+ this.logDebug("No containers to stop or remove");
153
+ return (0, rxjs_1.of)(false);
264
154
  }
265
- return output.asObservable();
155
+ return (0, rxjs_1.from)(Promise.all(containers.map((c) => this.docker.getContainer(c.Id).stop().catch((err) => {
156
+ if (err.statusCode !== 404)
157
+ throw err;
158
+ })))).pipe((0, operators_1.concatMap)(() => (0, rxjs_1.from)(Promise.all(containers.map((c) => this.docker.getContainer(c.Id).remove({ force: true }).catch((err) => {
159
+ if (err.statusCode !== 404)
160
+ throw err;
161
+ }))))), (0, operators_1.tap)(() => this.logDebug("All containers stopped and removed")), (0, operators_1.map)(() => true), (0, operators_1.catchError)((err) => {
162
+ this.logger.error(`Failed to stop/remove containers: ${err.message}`);
163
+ return (0, rxjs_1.of)(false);
164
+ }));
266
165
  }));
267
166
  }
268
- versionMismatch() {
269
- // Confirm the latest version is running, otherwise rip and replace
270
- return this.getRunningContainers().pipe((0, operators_1.map)((response) => {
271
- this.logger.debug(`versionMismatch - ${response}`);
272
- if (response) {
273
- const currentVersion = response[0];
274
- return !(currentVersion.RepoTags[0].includes(exports.DEFAULT_IMAGE_TAG));
167
+ deleteMismatchedImages() {
168
+ const imageRef = `${this.config.IMAGE_NAME}:${this.config.IMAGE_TAG}`;
169
+ return (0, rxjs_1.from)(this.docker.listImages()).pipe((0, operators_1.map)((images) => images
170
+ .filter((img) => img.RepoTags &&
171
+ img.RepoTags.some((tag) => tag.includes(this.config.IMAGE_NAME) && tag !== imageRef))
172
+ .map((img) => img.RepoTags[0])), (0, operators_1.concatMap)((imagesToRemove) => {
173
+ if (imagesToRemove.length === 0) {
174
+ this.logDebug("No mismatched images to delete");
175
+ return (0, rxjs_1.of)(false);
275
176
  }
276
- else
277
- return false;
177
+ return (0, rxjs_1.from)(Promise.all(imagesToRemove.map((tag) => this.docker.getImage(tag).remove()))).pipe((0, operators_1.tap)(() => {
178
+ this.userMessagesSubj.next("Removing extra images complete");
179
+ imagesToRemove.forEach((tag) => this.logDebug(`Removed image ${tag}`));
180
+ }), (0, operators_1.map)(() => true), (0, operators_1.catchError)((err) => {
181
+ this.logger.error(`Failed to delete images: ${err.message}`);
182
+ return (0, rxjs_1.of)(false);
183
+ }));
278
184
  }));
279
185
  }
280
- deleteRelayImage() {
281
- return this.getRelayImages().pipe((0, operators_1.map)((response) => {
282
- const output = new rxjs_1.Subject();
283
- this.logger.debug("Deleting relay images no longer targeted");
284
- if (response) {
285
- const localRelayImages = response;
286
- localRelayImages
287
- .filter(i => i.Tag != this.imageTag)
288
- .forEach(image => {
289
- this._cliExec.execAsyncStdOut(`docker rmi ${image.Repository}:${image.Tag}`, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
290
- .then((fullfulled) => {
291
- this.logger.debug('pull response', fullfulled);
292
- this.createContainer();
293
- output.next('Remove Image complete');
294
- this._userMessages.next(`Removing extra image(s) complete`);
295
- })
296
- .catch(error => {
297
- this.logger.debug(`Removing Relay image failed.`);
298
- });
299
- });
186
+ pullImage() {
187
+ const imageRef = `${this.config.IMAGE_NAME}:${this.config.IMAGE_TAG}`;
188
+ return (0, rxjs_1.from)(this.docker.getImage(imageRef).inspect()).pipe((0, operators_1.tap)(() => {
189
+ this.logDebug(`Image ${imageRef} already exists`);
190
+ this.userMessagesSubj.next(`Image ${imageRef} already exists`);
191
+ }), (0, operators_1.map)(() => true), (0, operators_1.catchError)((err) => {
192
+ if (err.statusCode !== 404) {
193
+ this.logger.error(`Error inspecting image ${imageRef}: ${err.message}`);
194
+ this.userMessagesSubj.next(`Error inspecting image: ${err.message}`);
195
+ return (0, rxjs_1.of)(false);
300
196
  }
301
- else
302
- output.next('Remove Image complete');
303
- output.subscribe();
304
- return output.asObservable();
197
+ this.logNotice(`Fetching image ${imageRef}. This may take a few minutes...`);
198
+ this.userMessagesSubj.next(`Fetching image ${imageRef}...`);
199
+ return (0, rxjs_1.from)(this.docker.pull(imageRef, {})).pipe((0, operators_1.concatMap)((stream) => new rxjs_1.Observable((observer) => {
200
+ this.docker.modem.followProgress(stream, (err) => {
201
+ if (err) {
202
+ this.logger.error(`Failed to pull image ${imageRef}: ${err.message}`);
203
+ this.userMessagesSubj.next(`Fetching failed: ${err.message}`);
204
+ observer.error(err);
205
+ return;
206
+ }
207
+ this.logDebug(`Image ${imageRef} pulled successfully`);
208
+ this.userMessagesSubj.next("Relay fetch complete");
209
+ observer.next(true);
210
+ observer.complete();
211
+ }, (progress) => { var _a; return this.userMessagesSubj.next(`Pulling ${(_a = progress.id) !== null && _a !== void 0 ? _a : imageRef} : Layer ${progress.status}`); });
212
+ })), (0, operators_1.catchError)((err) => {
213
+ this.logger.error(`Failed to pull image ${imageRef}: ${err.message}`);
214
+ this.userMessagesSubj.next(`Fetching failed: ${err.message}`);
215
+ return (0, rxjs_1.of)(false);
216
+ }));
305
217
  }));
306
218
  }
307
- getRelayImages(tag) {
308
- const command = `docker images --format "{{json .}}" ${this.imageName}${(tag) ? ":" + tag : ""}`;
309
- const output = new rxjs_1.Subject();
310
- this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
311
- .then((stdout) => {
312
- // Split the output into lines and parse each line as JSON
313
- try {
314
- if (stdout != undefined && stdout != "") {
315
- const lines = stdout.trim().split('\n');
316
- const images = lines.map(line => JSON.parse(line));
317
- output.next(images);
219
+ performHealthCheck() {
220
+ this.logDebug(`Checking ${this.config.CONTAINER_NAME} health...`);
221
+ return this.config.HEALTH_CHECK_URL.startsWith("http")
222
+ ? this.performHttpHealthCheck()
223
+ : this.performNatsHealthCheck();
224
+ }
225
+ performHttpHealthCheck() {
226
+ return (0, rxjs_1.from)(reqp.get(this.config.HEALTH_CHECK_URL)).pipe((0, operators_1.map)(() => {
227
+ this.logger.trace(`${this.config.CONTAINER_NAME} is healthy`);
228
+ this.readySubj.next(true);
229
+ this.connectionRefusedCount.next(0);
230
+ return true;
231
+ }), (0, operators_1.catchError)((err) => {
232
+ var _a;
233
+ if (((_a = err.error) === null || _a === void 0 ? void 0 : _a.code) === "ECONNREFUSED") {
234
+ this.connectionRefusedCount.next(this.connectionRefusedCount.value + 1);
235
+ if (this.connectionRefusedCount.value >= this.config.MAX_HEALTH_CHECK_RETRIES) {
236
+ this.logDebug("Max retries reached, restarting container");
237
+ this.startContainer().pipe((0, operators_1.take)(1)).subscribe();
238
+ this.connectionRefusedCount.next(0);
318
239
  }
319
- else
320
- output.next(undefined);
321
- }
322
- catch (parseError) {
323
- this.logger.error(`Error getting relay image: ${parseError}`);
324
- return parseError;
325
240
  }
326
- })
327
- .catch(error => {
328
- this.logger.debug(`get relay images failed.`);
329
- });
330
- return output.asObservable();
241
+ this.logDebug(`Health check failed: ${err.message}`);
242
+ this.readyFailedSubj.next();
243
+ return (0, rxjs_1.of)(false);
244
+ }));
331
245
  }
332
- dockerSetRelayRestart() {
333
- const command = `docker update --restart unless-stopped ${this.containerName}`;
334
- const output = new rxjs_1.Subject();
335
- this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
336
- .then((stdout) => {
337
- // Split the output into lines and parse each line as JSON
338
- try {
339
- output.next(undefined);
246
+ performNatsHealthCheck() {
247
+ return (0, rxjs_1.from)((() => tslib_1.__awaiter(this, void 0, void 0, function* () {
248
+ if (!this.natsClient.value || this.natsClient.value.isClosed()) {
249
+ this.natsClient.next(yield Transport.connect({ servers: this.config.HEALTH_CHECK_URL }));
340
250
  }
341
- catch (parseError) {
342
- return parseError;
251
+ return this.natsClient.value.request("dmz.get.heartbeat-json", Nats.Empty, { timeout: 2000 });
252
+ }))()).pipe((0, operators_1.map)(() => {
253
+ this.logger.trace(`${this.config.CONTAINER_NAME} NATS heartbeat received`);
254
+ this.readySubj.next(true);
255
+ this.connectionRefusedCount.next(0);
256
+ return true;
257
+ }), (0, operators_1.catchError)((err) => {
258
+ this.connectionRefusedCount.next(this.connectionRefusedCount.value + 1);
259
+ if (this.connectionRefusedCount.value >= this.config.MAX_HEALTH_CHECK_RETRIES) {
260
+ this.logDebug("Max retries reached, restarting container");
261
+ this.startContainer().pipe((0, operators_1.take)(1)).subscribe();
262
+ this.connectionRefusedCount.next(0);
343
263
  }
344
- })
345
- .catch(error => {
346
- this.logger.debug(`dockerSetRelayRestart - get running containers Failed.`);
347
- output.next(undefined);
348
- });
349
- return output.asObservable();
350
- }
351
- healthCheck() {
352
- var _a, _b;
353
- try {
354
- this.logger.debug(`healthCheck - Checking ${this.containerName} health now...`);
355
- reqp.get(this.healthCheckUrl)
356
- .then(response => {
357
- this.logger.trace(`healthCheck - ...${this.containerName} ready.`);
358
- // this.watchLogs();
359
- this.readySubj.next(true);
360
- })
361
- .catch(error => {
362
- var _a, _b;
363
- // Sometimes the relay container hangs and the heartbeat is never reachable.
364
- // Since we're pinging it every second, let's simply give up and restart the container after bunch of tries
365
- if (error.error.code == "ECONNREFUSED") {
366
- this.connectionRefusedCount = this.connectionRefusedCount + 1;
367
- if (this.connectionRefusedCount > this.MAXTRIESBEFORECONTAINERRESTART) {
368
- this.createContainer().pipe((0, operators_1.take)(1)).subscribe();
369
- this.connectionRefusedCount = 0;
370
- }
264
+ this.logDebug(`NATS health check failed: ${err.message}`);
265
+ this.readyFailedSubj.next();
266
+ return (0, rxjs_1.of)(false);
267
+ }), (0, operators_1.tap)({
268
+ complete: () => {
269
+ if (this.natsClient.value && !this.natsClient.value.isClosed()) {
270
+ this.natsClient.value.close().catch((err) => this.logger.error(`Failed to close NATS client: ${err.message}`));
271
+ this.natsClient.next(null);
371
272
  }
372
- this.logger.debug(`healthCheck - ...${this.containerName} failed to respond (${(_a = error.name) !== null && _a !== void 0 ? _a : ""} ${(_b = error.message) !== null && _b !== void 0 ? _b : ""})`);
373
- this.readyFailed$.next();
374
- // this.startSubj.next(this.containerName);
375
- });
376
- }
377
- catch (error) {
378
- this.logger.debug(`healthCheck failed (${(_a = error.name) !== null && _a !== void 0 ? _a : ""} ${(_b = error.message) !== null && _b !== void 0 ? _b : ""})`, JSON.stringify(error, null, 2));
379
- this.readyFailed$.next();
380
- }
381
- }
382
- stop() {
383
- // fire off this command in a separate cli window
384
- let cmdStop = `docker stop ${this.containerName}`;
385
- let result$ = new rxjs_1.Subject();
386
- result$.subscribe();
387
- let options = {
388
- cwd: this.projectRoot,
389
- // stdio: "inherit",
390
- // stdio: [process.stdin, process.stdout, process.stderr, process.st], // TODO: Capture stderr instead
391
- shell: true
392
- };
393
- const ls = (0, child_process_1.spawn)(cmdStop, options);
394
- // ls.stdout?.on('data', (data: any) => {
395
- // result$.next(true);
396
- // });
397
- // ls.stdout?.on('error', (data: any) => {
398
- // result$.next(false);
399
- // });
400
- // ls.on('error', (data: any) => {
401
- // result$.next(false);
402
- // });
403
- ls.on('close', (data) => {
404
- result$.next(false);
405
- });
406
- return result$.asObservable();
407
- }
408
- remove() {
409
- // fire off this command in a separate cli window
410
- this.getAllContainersOfRelayImage().pipe((0, operators_1.map)(response => {
411
- if (response && response.length > 0) {
412
- response.forEach(containerId => {
413
- let cmdStop = `docker rm ${containerId} -f`;
414
- let options = {
415
- cwd: this.projectRoot,
416
- shell: true
417
- };
418
- const ls = (0, child_process_1.spawn)(cmdStop, options);
419
- ls.on('close', (data) => {
420
- result$.next(false);
421
- });
422
- });
423
273
  }
424
- }))
425
- .subscribe();
426
- let result$ = new rxjs_1.Subject();
427
- result$.subscribe();
428
- return result$.asObservable();
274
+ }));
429
275
  }
430
- destroy() {
431
- throw new codiac_error_1.CodiacError("Not yet implemented");
276
+ startContainer() {
277
+ return this.createLocalDataFolders().pipe((0, operators_1.concatMap)(() => this.pullImage()), (0, operators_1.concatMap)(() => this.createContainer()), (0, operators_1.concatMap)(() => this.startContainerInternal()), (0, operators_1.catchError)((err) => {
278
+ this.logger.error(`Failed to start container: ${err.message}`);
279
+ this.userMessagesSubj.next(`Failed to start container: ${err.message}`);
280
+ return (0, rxjs_1.of)(false);
281
+ }));
432
282
  }
433
283
  createContainer() {
434
- var _a, _b;
435
- this.logger.debug(`createContainer - Creating ${this.containerName} from image: ${this.imageName}:${this.imageTag}`);
436
- let container = {
437
- Image: `${this.imageName}:${this.imageTag}`,
438
- name: `${this.containerName}`,
439
- Hostname: `${this.containerName}`,
440
- ExposedPorts: {
441
- "5799/tcp": {},
442
- "5798/tcp": {},
443
- "4222/tcp": {},
444
- "9222/tcp": {}
445
- },
284
+ var _a;
285
+ const containerConfig = {
286
+ Image: `${this.config.IMAGE_NAME}:${this.config.IMAGE_TAG}`,
287
+ name: this.config.CONTAINER_NAME,
288
+ Hostname: this.config.CONTAINER_NAME,
289
+ ExposedPorts: Object.assign({}, DEFAULT_CONFIG.EXPOSED_PORTS),
446
290
  HostConfig: {
447
- Binds: [
448
- `${os.homedir}/.codiac:/root/.codiac`
449
- // `${os.homedir}/.kube:/root/.kube`,
450
- // `${os.homedir}/.azure:/root/.azure`
451
- ],
452
- PortBindings: {
453
- "5799/tcp": [
454
- {
455
- HostPort: "5799"
456
- }
457
- ],
458
- "5798/tcp": [
459
- {
460
- HostPort: "5798"
461
- }
462
- ],
463
- "4222/tcp": [
464
- {
465
- HostPort: "4222"
466
- }
467
- ],
468
- "9222/tcp": [
469
- {
470
- HostPort: "9222"
471
- }
472
- ]
473
- },
474
- RestartPolicy: {
475
- Name: "unless-stopped"
476
- }
291
+ Binds: [...DEFAULT_CONFIG.BINDS],
292
+ PortBindings: Object.assign({}, DEFAULT_CONFIG.PORT_BINDINGS),
293
+ RestartPolicy: { Name: "unless-stopped" },
477
294
  },
478
- Volumes: {
479
- "/root/.codiac": {}
480
- // "/root/.kube": {},
481
- // "/root/.azure": {}
482
- },
483
- Env: [
484
- "HOST_PLATFORM=".concat(process.platform)
485
- ]
295
+ Volumes: Object.assign({}, DEFAULT_CONFIG.VOLUMES),
296
+ Env: [`HOST_PLATFORM=${process.platform}`],
486
297
  };
487
- if (this.localEntities == true) {
488
- container.ExposedPorts["27017/tcp"] = {};
489
- container.HostConfig.PortBindings["27017/tcp"] = [{ HostPort: "27017" }];
490
- (_a = container.HostConfig.Binds) === null || _a === void 0 ? void 0 : _a.push(`${os.homedir}/.codiac/data/db:/data/db`);
491
- (_b = container.HostConfig.Binds) === null || _b === void 0 ? void 0 : _b.push(`${os.homedir}/.codiac/data/logs:/data/logs`);
492
- container.Volumes["/data/db"] = {};
493
- container.Volumes["/data/logs"] = {};
494
- container.Entrypoint = ["./start-relay-w-nats-and-local-entities.sh"];
298
+ if (this.localEntities) {
299
+ containerConfig.ExposedPorts["27017/tcp"] = {};
300
+ containerConfig.HostConfig.PortBindings["27017/tcp"] = [{ HostPort: "27017" }];
301
+ (_a = containerConfig.HostConfig.Binds) === null || _a === void 0 ? void 0 : _a.push(`${os.homedir()}/.codiac/data/db:/data/db`, `${os.homedir()}/.codiac/data/logs:/data/logs`);
302
+ containerConfig.Volumes["/data/db"] = {};
303
+ containerConfig.Volumes["/data/logs"] = {};
304
+ containerConfig.Entrypoint = ["./start-relay-w-nats-and-local-entities.sh"];
495
305
  }
496
- const output = new rxjs_1.Subject();
497
- this.docker.createContainer(container)
498
- .then(container => {
499
- output.next(container);
500
- this.logger.trace(`createContainer - Created ${this.containerName}`);
501
- this.execStart();
502
- })
503
- .catch(error => {
504
- if (error.statusCode == 409)
505
- this.execStart(); //Already in use
506
- else {
507
- this.logger.debug(`createContainer - Failed to create ${this.containerName}`, JSON.stringify(error, null, 2));
508
- this.pullImage();
306
+ return (0, rxjs_1.from)(this.docker.createContainer(containerConfig)).pipe((0, operators_1.tap)(() => this.logDebug(`Created container ${this.config.CONTAINER_NAME}`)), (0, operators_1.map)(() => true), (0, operators_1.catchError)((err) => {
307
+ if (err.statusCode === 409) {
308
+ this.logDebug(`Container ${this.config.CONTAINER_NAME} already exists`);
309
+ return (0, rxjs_1.of)(true);
509
310
  }
510
- output.next(error);
511
- });
512
- output.subscribe();
513
- return output.asObservable();
311
+ this.logger.error(`Failed to create container: ${err.message}`);
312
+ return this.pullImage();
313
+ }));
514
314
  }
515
- pullImage() {
516
- const output = new rxjs_1.Subject();
517
- this.logger.notice("Fetching Codiac Relay image. This may take a few minutes...");
518
- this._userMessages.next(`Fetching Codiac Relay image. This may take a few minutes...`);
519
- this._cliExec.execAsyncStdOut(`docker pull ${this.imageName}:${this.imageTag}`, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
520
- .then((fullfulled) => {
521
- this.logger.debug('pull response', fullfulled);
522
- this.createContainer();
523
- output.next('pull complete');
524
- this._userMessages.next(`Fetch complete`);
525
- })
526
- .catch(error => {
527
- const msg = `Fetching failed: ${error.stderr}`;
528
- this.logger.error(msg);
529
- this._userMessages.next(msg);
530
- });
531
- output.subscribe();
532
- return output.asObservable();
315
+ startContainerInternal() {
316
+ return (0, rxjs_1.from)(this.docker.getContainer(this.config.CONTAINER_NAME).inspect()).pipe((0, operators_1.concatMap)((info) => {
317
+ if (info.State.Running) {
318
+ this.logDebug(`Container ${this.config.CONTAINER_NAME} is already running`);
319
+ return this.performHealthCheck();
320
+ }
321
+ return (0, rxjs_1.from)(this.docker.getContainer(this.config.CONTAINER_NAME).start()).pipe((0, operators_1.tap)(() => {
322
+ this.logDebug(`Started container ${this.config.CONTAINER_NAME}`);
323
+ this.userMessagesSubj.next("Starting Relay...");
324
+ this.performHealthCheck().subscribe();
325
+ }), (0, operators_1.map)(() => true));
326
+ }), (0, operators_1.catchError)((err) => {
327
+ if (err.statusCode === 404) {
328
+ this.logDebug(`Container ${this.config.CONTAINER_NAME} not found, creating...`);
329
+ return this.createContainer();
330
+ }
331
+ this.logger.error(`Failed to start container: ${err.message}`);
332
+ return (0, rxjs_1.of)(false);
333
+ }));
533
334
  }
534
- execStart() {
535
- (0, rxjs_1.of)(this.createLocalDataFolders()) // Run host machine configs first, then proceed to start the container.
536
- .pipe((0, operators_1.map)(response => {
537
- this.docker.getContainer(this.containerName).start().then(container => {
538
- this._userMessages.next(`Starting Relay...`);
539
- this.dockerSetRelayRestart().pipe((0, operators_1.map)(_ => this.healthCheck())).subscribe();
540
- this.logger.debug(`execStart - Container ${container.id} started`);
541
- })
542
- .catch(error => {
543
- this.logger.debug(`execStart - Failed to start ${this.containerName}`, error.message);
544
- if (error.statusCode == 404)
545
- this.createContainer().pipe((0, operators_1.take)(1)).subscribe();
546
- // If it's all gone to hell and that server isnt responding. kill it and try again.
547
- if (error.statusCode == 500)
548
- this.serverErrorCount = this.serverErrorCount + 1;
549
- // Hard stop on 20 tries, reboot it
550
- if (this.serverErrorCount == 20) {
551
- this.stop()
552
- .pipe((0, operators_1.map)(x => this.startFailedSubj.next(this.containerName)))
553
- .pipe((0, operators_1.take)(1))
554
- .subscribe();
555
- this.serverErrorCount = 0;
556
- }
557
- });
558
- }))
559
- .subscribe();
335
+ stop() {
336
+ return (0, rxjs_1.from)(this.docker.getContainer(this.config.CONTAINER_NAME).inspect()).pipe((0, operators_1.concatMap)((info) => {
337
+ if (!info.State.Running) {
338
+ this.logDebug(`Container ${this.config.CONTAINER_NAME} is not running`);
339
+ return (0, rxjs_1.of)(false);
340
+ }
341
+ return (0, rxjs_1.from)(this.docker.getContainer(this.config.CONTAINER_NAME).stop()).pipe((0, operators_1.tap)(() => this.logDebug(`Stopped container ${this.config.CONTAINER_NAME}`)), (0, operators_1.map)(() => true));
342
+ }), (0, operators_1.catchError)((err) => {
343
+ if (err.statusCode === 404) {
344
+ this.logDebug(`Container ${this.config.CONTAINER_NAME} does not exist`);
345
+ return (0, rxjs_1.of)(false);
346
+ }
347
+ this.logger.error(`Failed to stop container: ${err.message}`);
348
+ return (0, rxjs_1.of)(false);
349
+ }));
560
350
  }
561
- watchLogs() {
562
- const container = this.docker.getContainer(this.containerName);
563
- container.logs({
564
- follow: true,
565
- stdout: true,
566
- stderr: true
567
- })
568
- .then(stream => {
569
- this.promisifyStream(stream);
570
- });
351
+ remove() {
352
+ return this.stop().pipe((0, operators_1.concatMap)(() => (0, rxjs_1.from)(this.docker.getContainer(this.config.CONTAINER_NAME).remove({ force: true })).pipe((0, operators_1.tap)(() => this.logDebug(`Removed container ${this.config.CONTAINER_NAME}`)), (0, operators_1.map)(() => true), (0, operators_1.catchError)((err) => {
353
+ if (err.statusCode === 404) {
354
+ this.logDebug(`Container ${this.config.CONTAINER_NAME} does not exist`);
355
+ return (0, rxjs_1.of)(false);
356
+ }
357
+ this.logger.error(`Failed to remove container: ${err.message}`);
358
+ return (0, rxjs_1.of)(false);
359
+ }))));
571
360
  }
572
- getDocker() {
573
- this.logger.debug("getDocker - initialize");
574
- const docker = this.socketPath ? new Docker({ socketPath: this.socketPath }) : new Docker();
575
- (0, rxjs_1.from)(docker.info())
576
- .pipe((0, operators_1.map)((info) => {
577
- this.logger.debug("getDocker - check docker info");
578
- if (!info)
579
- throw null;
580
- this.docker = docker;
581
- this.dockerSubj.next(docker);
582
- this.logger.debug("Docker is running");
583
- return this.docker;
584
- }))
585
- .pipe((0, operators_1.catchError)(error => {
586
- // cli.action.stop();
587
- const msg = 'Can you start docker for me?';
588
- this.logger.error(msg);
589
- this._userMessages.next(msg);
590
- this.dockerFailedSubj.next();
591
- return (0, rxjs_1.of)(this.docker);
592
- }))
593
- .pipe((0, operators_1.take)(1))
594
- .subscribe();
361
+ destroy() {
362
+ return this.stop().pipe((0, operators_1.concatMap)(() => this.remove()), (0, operators_1.tap)(() => {
363
+ if (this.natsClient.value && !this.natsClient.value.isClosed()) {
364
+ this.natsClient.value.close().catch((err) => this.logger.error(`Failed to close NATS client: ${err.message}`));
365
+ this.natsClient.next(null);
366
+ }
367
+ this.destroySubj.next();
368
+ this.destroySubj.complete();
369
+ this.readySubj.complete();
370
+ this.readyFailedSubj.complete();
371
+ this.userMessagesSubj.complete();
372
+ this.connectionRefusedCount.complete();
373
+ this.logDebug("RelayContainer destroyed");
374
+ }), (0, operators_1.map)(() => true), (0, operators_1.catchError)((err) => {
375
+ this.logger.error(`Failed to destroy RelayContainer: ${err.message}`);
376
+ return (0, rxjs_1.of)(false);
377
+ }));
595
378
  }
596
379
  };
597
380
  RelayContainer = tslib_1.__decorate([
598
- tslib_1.__param(0, (0, inversify_1.inject)(INTERFACES_CLI_1.INTERFACES_CLI.IOutputStyler)),
381
+ tslib_1.__param(0, (0, inversify_1.inject)(ops_1.INTERFACES_CLI.IOutputStyler)),
599
382
  tslib_1.__param(1, (0, inversify_1.inject)(entities_1.CliEnviro)),
600
383
  tslib_1.__param(2, (0, inversify_1.inject)(codiac_common_1.INTERFACES.IBetterLogger)),
601
384
  tslib_1.__metadata("design:paramtypes", [Object, entities_1.CliEnviro, Object, String, Number, String, Boolean])
602
385
  ], RelayContainer);
603
386
  exports.RelayContainer = RelayContainer;
387
+ exports.DEFAULT_IMAGE_TAG = DEFAULT_CONFIG.IMAGE_TAG;
604
388
  //# sourceMappingURL=relay-container.js.map