@kapeta/local-cluster-service 0.33.5 → 0.33.7

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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [0.33.7](https://github.com/kapetacom/local-cluster-service/compare/v0.33.6...v0.33.7) (2024-01-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Re-pull "latest" images every 15 mins ([#116](https://github.com/kapetacom/local-cluster-service/issues/116)) ([4b71fcc](https://github.com/kapetacom/local-cluster-service/commit/4b71fcce131dcd5b6cea955dc663b93aa3c3c930))
7
+
8
+ ## [0.33.6](https://github.com/kapetacom/local-cluster-service/compare/v0.33.5...v0.33.6) (2024-01-09)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Catch errors when starting ([#115](https://github.com/kapetacom/local-cluster-service/issues/115)) ([00f1875](https://github.com/kapetacom/local-cluster-service/commit/00f18758063e7ea5cb20d3a70dbd1706ef9f4c42))
14
+ * Use async when possible and check exists before using realpath ([#114](https://github.com/kapetacom/local-cluster-service/issues/114)) ([9abb498](https://github.com/kapetacom/local-cluster-service/commit/9abb498191b68952f8789a800cc6ad8b2defcb20))
15
+
1
16
  ## [0.33.5](https://github.com/kapetacom/local-cluster-service/compare/v0.33.4...v0.33.5) (2024-01-06)
2
17
 
3
18
 
@@ -25,7 +25,7 @@ const CACHE_TTL = 60 * 60 * 1000; // 1 hour
25
25
  const UPGRADE_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
26
26
  const toKey = (ref) => `assetManager:asset:${ref}`;
27
27
  function filterExists(asset) {
28
- return fs_extra_1.default.existsSync(asset.path);
28
+ return fs_extra_1.default.existsSync(asset.path) && fs_extra_1.default.existsSync(asset.ymlPath);
29
29
  }
30
30
  function enrichAsset(asset) {
31
31
  return {
@@ -43,6 +43,7 @@ declare class ContainerManager {
43
43
  private _version;
44
44
  private _lastDockerAccessCheck;
45
45
  private logStreams;
46
+ private _latestImagePulls;
46
47
  constructor();
47
48
  initialize(): Promise<void>;
48
49
  checkAlive(): Promise<boolean>;
@@ -27,6 +27,7 @@ exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
27
27
  const NANO_SECOND = 1000000;
28
28
  const HEALTH_CHECK_INTERVAL = 3000;
29
29
  const HEALTH_CHECK_MAX = 100;
30
+ const LATEST_PULL_TIMEOUT = 1000 * 60 * 15; // 15 minutes
30
31
  exports.COMPOSE_LABEL_PROJECT = 'com.docker.compose.project';
31
32
  exports.COMPOSE_LABEL_SERVICE = 'com.docker.compose.service';
32
33
  exports.HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
@@ -69,11 +70,13 @@ class ContainerManager {
69
70
  _version;
70
71
  _lastDockerAccessCheck = 0;
71
72
  logStreams = {};
73
+ _latestImagePulls = {};
72
74
  constructor() {
73
75
  this._docker = null;
74
76
  this._alive = false;
75
77
  this._version = '';
76
78
  this._mountDir = path_1.default.join(storageService_1.storageService.getKapetaBasedir(), 'mounts');
79
+ this._latestImagePulls = {};
77
80
  fs_extra_1.default.mkdirpSync(this._mountDir);
78
81
  }
79
82
  async initialize() {
@@ -221,7 +224,20 @@ class ContainerManager {
221
224
  const imageTagList = (await this.docker().listImages({}))
222
225
  .filter((imageData) => !!imageData.RepoTags)
223
226
  .map((imageData) => imageData.RepoTags);
224
- if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
227
+ const imageExists = imageTagList.some((imageTags) => imageTags.includes(image));
228
+ if (tag === 'latest') {
229
+ if (imageExists && this._latestImagePulls[imageName]) {
230
+ const lastPull = this._latestImagePulls[imageName];
231
+ const timeSinceLastPull = Date.now() - lastPull;
232
+ if (timeSinceLastPull < LATEST_PULL_TIMEOUT) {
233
+ console.log('Image found and was pulled %s seconds ago: %s', Math.round(timeSinceLastPull / 1000), image);
234
+ // Last pull was less than the timeout - don't pull again
235
+ return false;
236
+ }
237
+ }
238
+ this._latestImagePulls[imageName] = Date.now();
239
+ }
240
+ else if (imageExists) {
225
241
  console.log('Image found: %s', image);
226
242
  return false;
227
243
  }
@@ -6,6 +6,7 @@ import { DefinitionInfo } from '@kapeta/local-cluster-config';
6
6
  export declare const SAMPLE_PLAN_NAME = "kapeta/sample-java-chat-plan";
7
7
  declare class DefinitionsManager {
8
8
  private resolveDefinitionsAndSamples;
9
+ private prepareSample;
9
10
  private applyFilters;
10
11
  getDefinitions(kindFilter?: string | string[]): Promise<DefinitionInfo[]>;
11
12
  exists(ref: string): Promise<boolean>;
@@ -58,6 +58,16 @@ class DefinitionsManager {
58
58
  // Not logged in yet, so we can't rewrite the sample plan
59
59
  return definitions;
60
60
  }
61
+ try {
62
+ await this.prepareSample(definitions, samplePlan, profile);
63
+ }
64
+ catch (e) {
65
+ console.warn('Failed to prepare sample plan', e);
66
+ }
67
+ // Return the rewritten definitions
68
+ return local_cluster_config_1.default.getDefinitions();
69
+ }
70
+ async prepareSample(definitions, samplePlan, profile) {
61
71
  const newName = getRenamed(samplePlan, profile.handle);
62
72
  if (definitions.some((d) => d.definition.metadata.name === newName && d.version === 'local')) {
63
73
  // We already have a local version of the sample plan
@@ -104,8 +114,6 @@ class DefinitionsManager {
104
114
  await nodejs_registry_utils_1.Actions.link(progressListener, asset.path);
105
115
  }
106
116
  console.log('Rewrite done for sample plan');
107
- // Return the rewritten definitions
108
- return local_cluster_config_1.default.getDefinitions();
109
117
  }
110
118
  applyFilters(definitions, kindFilter) {
111
119
  if (kindFilter.length === 0) {
@@ -40,11 +40,16 @@ router.get('/:systemId/instances/:instanceId', (req, res) => {
40
40
  * Start all instances in a plan
41
41
  */
42
42
  router.post('/:systemId/start', async (req, res) => {
43
- const task = await instanceManager_1.instanceManager.startAllForPlan(req.params.systemId);
44
- res.status(202).send({
45
- ok: true,
46
- taskId: task.id,
47
- });
43
+ try {
44
+ const task = await instanceManager_1.instanceManager.startAllForPlan(req.params.systemId);
45
+ res.status(202).send({
46
+ ok: true,
47
+ taskId: task.id,
48
+ });
49
+ }
50
+ catch (e) {
51
+ res.status(500).send({ ok: false, error: e.message });
52
+ }
48
53
  });
49
54
  /**
50
55
  * Stop all instances in plan
@@ -60,19 +65,24 @@ router.post('/:systemId/stop', async (req, res) => {
60
65
  * Start single instance in a plan
61
66
  */
62
67
  router.post('/:systemId/:instanceId/start', async (req, res) => {
63
- const result = await instanceManager_1.instanceManager.start(req.params.systemId, req.params.instanceId);
64
- if (result instanceof taskManager_1.Task) {
65
- res.status(202).send({
66
- ok: true,
67
- taskId: result.id,
68
- });
68
+ try {
69
+ const result = await instanceManager_1.instanceManager.start(req.params.systemId, req.params.instanceId);
70
+ if (result instanceof taskManager_1.Task) {
71
+ res.status(202).send({
72
+ ok: true,
73
+ taskId: result.id,
74
+ });
75
+ }
76
+ else {
77
+ res.status(202).send({
78
+ ok: true,
79
+ pid: result.pid,
80
+ type: result.type,
81
+ });
82
+ }
69
83
  }
70
- else {
71
- res.status(202).send({
72
- ok: true,
73
- pid: result.pid,
74
- type: result.type,
75
- });
84
+ catch (e) {
85
+ res.status(500).send({ ok: false, error: e.message });
76
86
  }
77
87
  });
78
88
  /**
@@ -8,7 +8,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.BlockInstanceRunner = exports.resolvePortType = void 0;
11
- const node_fs_1 = __importDefault(require("node:fs"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
12
  const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
13
13
  const utils_1 = require("./utils");
14
14
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
@@ -130,7 +130,7 @@ class BlockInstanceRunner {
130
130
  */
131
131
  async _startLocalProcess(blockInstance, blockInfo, env, assetVersion) {
132
132
  const baseDir = local_cluster_config_1.default.getRepositoryAssetPath(blockInfo.handle, blockInfo.name, blockInfo.version);
133
- if (!node_fs_1.default.existsSync(baseDir)) {
133
+ if (!fs_extra_1.default.existsSync(baseDir)) {
134
134
  throw new Error(`Local block not registered correctly - expected symlink here: ${baseDir}.\n` +
135
135
  `Make sure you've run "kap registry link" in your local directory to connect it to Kapeta`);
136
136
  }
@@ -173,7 +173,7 @@ class BlockInstanceRunner {
173
173
  if (localContainer.healthcheck) {
174
174
  HealthCheck = containerManager_1.containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
175
175
  }
176
- const realLocalPath = node_fs_1.default.realpathSync(baseDir);
176
+ const realLocalPath = await fs_extra_1.default.realpath(baseDir);
177
177
  const Mounts = containerManager_1.containerManager.toDockerMounts({
178
178
  [workingDir]: (0, containerManager_1.toLocalBindVolume)(realLocalPath),
179
179
  });
@@ -224,7 +224,7 @@ class BlockInstanceRunner {
224
224
  async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
225
225
  const { versionFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
226
226
  const versionYml = versionFile;
227
- if (!node_fs_1.default.existsSync(versionYml)) {
227
+ if (!fs_extra_1.default.existsSync(versionYml)) {
228
228
  throw new Error(`Did not find version info at the expected path: ${versionYml}`);
229
229
  }
230
230
  const versionInfo = (0, utils_1.readYML)(versionYml);
@@ -280,7 +280,7 @@ class BlockInstanceRunner {
280
280
  async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
281
281
  const { assetFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockUri.handle, blockUri.name, blockUri.version);
282
282
  const kapetaYmlPath = assetFile;
283
- if (!node_fs_1.default.existsSync(kapetaYmlPath)) {
283
+ if (!fs_extra_1.default.existsSync(kapetaYmlPath)) {
284
284
  throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
285
285
  }
286
286
  const spec = providerDefinition.definition.spec;
@@ -25,7 +25,7 @@ const CACHE_TTL = 60 * 60 * 1000; // 1 hour
25
25
  const UPGRADE_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
26
26
  const toKey = (ref) => `assetManager:asset:${ref}`;
27
27
  function filterExists(asset) {
28
- return fs_extra_1.default.existsSync(asset.path);
28
+ return fs_extra_1.default.existsSync(asset.path) && fs_extra_1.default.existsSync(asset.ymlPath);
29
29
  }
30
30
  function enrichAsset(asset) {
31
31
  return {
@@ -43,6 +43,7 @@ declare class ContainerManager {
43
43
  private _version;
44
44
  private _lastDockerAccessCheck;
45
45
  private logStreams;
46
+ private _latestImagePulls;
46
47
  constructor();
47
48
  initialize(): Promise<void>;
48
49
  checkAlive(): Promise<boolean>;
@@ -27,6 +27,7 @@ exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
27
27
  const NANO_SECOND = 1000000;
28
28
  const HEALTH_CHECK_INTERVAL = 3000;
29
29
  const HEALTH_CHECK_MAX = 100;
30
+ const LATEST_PULL_TIMEOUT = 1000 * 60 * 15; // 15 minutes
30
31
  exports.COMPOSE_LABEL_PROJECT = 'com.docker.compose.project';
31
32
  exports.COMPOSE_LABEL_SERVICE = 'com.docker.compose.service';
32
33
  exports.HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
@@ -69,11 +70,13 @@ class ContainerManager {
69
70
  _version;
70
71
  _lastDockerAccessCheck = 0;
71
72
  logStreams = {};
73
+ _latestImagePulls = {};
72
74
  constructor() {
73
75
  this._docker = null;
74
76
  this._alive = false;
75
77
  this._version = '';
76
78
  this._mountDir = path_1.default.join(storageService_1.storageService.getKapetaBasedir(), 'mounts');
79
+ this._latestImagePulls = {};
77
80
  fs_extra_1.default.mkdirpSync(this._mountDir);
78
81
  }
79
82
  async initialize() {
@@ -221,7 +224,20 @@ class ContainerManager {
221
224
  const imageTagList = (await this.docker().listImages({}))
222
225
  .filter((imageData) => !!imageData.RepoTags)
223
226
  .map((imageData) => imageData.RepoTags);
224
- if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
227
+ const imageExists = imageTagList.some((imageTags) => imageTags.includes(image));
228
+ if (tag === 'latest') {
229
+ if (imageExists && this._latestImagePulls[imageName]) {
230
+ const lastPull = this._latestImagePulls[imageName];
231
+ const timeSinceLastPull = Date.now() - lastPull;
232
+ if (timeSinceLastPull < LATEST_PULL_TIMEOUT) {
233
+ console.log('Image found and was pulled %s seconds ago: %s', Math.round(timeSinceLastPull / 1000), image);
234
+ // Last pull was less than the timeout - don't pull again
235
+ return false;
236
+ }
237
+ }
238
+ this._latestImagePulls[imageName] = Date.now();
239
+ }
240
+ else if (imageExists) {
225
241
  console.log('Image found: %s', image);
226
242
  return false;
227
243
  }
@@ -6,6 +6,7 @@ import { DefinitionInfo } from '@kapeta/local-cluster-config';
6
6
  export declare const SAMPLE_PLAN_NAME = "kapeta/sample-java-chat-plan";
7
7
  declare class DefinitionsManager {
8
8
  private resolveDefinitionsAndSamples;
9
+ private prepareSample;
9
10
  private applyFilters;
10
11
  getDefinitions(kindFilter?: string | string[]): Promise<DefinitionInfo[]>;
11
12
  exists(ref: string): Promise<boolean>;
@@ -58,6 +58,16 @@ class DefinitionsManager {
58
58
  // Not logged in yet, so we can't rewrite the sample plan
59
59
  return definitions;
60
60
  }
61
+ try {
62
+ await this.prepareSample(definitions, samplePlan, profile);
63
+ }
64
+ catch (e) {
65
+ console.warn('Failed to prepare sample plan', e);
66
+ }
67
+ // Return the rewritten definitions
68
+ return local_cluster_config_1.default.getDefinitions();
69
+ }
70
+ async prepareSample(definitions, samplePlan, profile) {
61
71
  const newName = getRenamed(samplePlan, profile.handle);
62
72
  if (definitions.some((d) => d.definition.metadata.name === newName && d.version === 'local')) {
63
73
  // We already have a local version of the sample plan
@@ -104,8 +114,6 @@ class DefinitionsManager {
104
114
  await nodejs_registry_utils_1.Actions.link(progressListener, asset.path);
105
115
  }
106
116
  console.log('Rewrite done for sample plan');
107
- // Return the rewritten definitions
108
- return local_cluster_config_1.default.getDefinitions();
109
117
  }
110
118
  applyFilters(definitions, kindFilter) {
111
119
  if (kindFilter.length === 0) {
@@ -40,11 +40,16 @@ router.get('/:systemId/instances/:instanceId', (req, res) => {
40
40
  * Start all instances in a plan
41
41
  */
42
42
  router.post('/:systemId/start', async (req, res) => {
43
- const task = await instanceManager_1.instanceManager.startAllForPlan(req.params.systemId);
44
- res.status(202).send({
45
- ok: true,
46
- taskId: task.id,
47
- });
43
+ try {
44
+ const task = await instanceManager_1.instanceManager.startAllForPlan(req.params.systemId);
45
+ res.status(202).send({
46
+ ok: true,
47
+ taskId: task.id,
48
+ });
49
+ }
50
+ catch (e) {
51
+ res.status(500).send({ ok: false, error: e.message });
52
+ }
48
53
  });
49
54
  /**
50
55
  * Stop all instances in plan
@@ -60,19 +65,24 @@ router.post('/:systemId/stop', async (req, res) => {
60
65
  * Start single instance in a plan
61
66
  */
62
67
  router.post('/:systemId/:instanceId/start', async (req, res) => {
63
- const result = await instanceManager_1.instanceManager.start(req.params.systemId, req.params.instanceId);
64
- if (result instanceof taskManager_1.Task) {
65
- res.status(202).send({
66
- ok: true,
67
- taskId: result.id,
68
- });
68
+ try {
69
+ const result = await instanceManager_1.instanceManager.start(req.params.systemId, req.params.instanceId);
70
+ if (result instanceof taskManager_1.Task) {
71
+ res.status(202).send({
72
+ ok: true,
73
+ taskId: result.id,
74
+ });
75
+ }
76
+ else {
77
+ res.status(202).send({
78
+ ok: true,
79
+ pid: result.pid,
80
+ type: result.type,
81
+ });
82
+ }
69
83
  }
70
- else {
71
- res.status(202).send({
72
- ok: true,
73
- pid: result.pid,
74
- type: result.type,
75
- });
84
+ catch (e) {
85
+ res.status(500).send({ ok: false, error: e.message });
76
86
  }
77
87
  });
78
88
  /**
@@ -8,7 +8,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.BlockInstanceRunner = exports.resolvePortType = void 0;
11
- const node_fs_1 = __importDefault(require("node:fs"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
12
  const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
13
13
  const utils_1 = require("./utils");
14
14
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
@@ -130,7 +130,7 @@ class BlockInstanceRunner {
130
130
  */
131
131
  async _startLocalProcess(blockInstance, blockInfo, env, assetVersion) {
132
132
  const baseDir = local_cluster_config_1.default.getRepositoryAssetPath(blockInfo.handle, blockInfo.name, blockInfo.version);
133
- if (!node_fs_1.default.existsSync(baseDir)) {
133
+ if (!fs_extra_1.default.existsSync(baseDir)) {
134
134
  throw new Error(`Local block not registered correctly - expected symlink here: ${baseDir}.\n` +
135
135
  `Make sure you've run "kap registry link" in your local directory to connect it to Kapeta`);
136
136
  }
@@ -173,7 +173,7 @@ class BlockInstanceRunner {
173
173
  if (localContainer.healthcheck) {
174
174
  HealthCheck = containerManager_1.containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
175
175
  }
176
- const realLocalPath = node_fs_1.default.realpathSync(baseDir);
176
+ const realLocalPath = await fs_extra_1.default.realpath(baseDir);
177
177
  const Mounts = containerManager_1.containerManager.toDockerMounts({
178
178
  [workingDir]: (0, containerManager_1.toLocalBindVolume)(realLocalPath),
179
179
  });
@@ -224,7 +224,7 @@ class BlockInstanceRunner {
224
224
  async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
225
225
  const { versionFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
226
226
  const versionYml = versionFile;
227
- if (!node_fs_1.default.existsSync(versionYml)) {
227
+ if (!fs_extra_1.default.existsSync(versionYml)) {
228
228
  throw new Error(`Did not find version info at the expected path: ${versionYml}`);
229
229
  }
230
230
  const versionInfo = (0, utils_1.readYML)(versionYml);
@@ -280,7 +280,7 @@ class BlockInstanceRunner {
280
280
  async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
281
281
  const { assetFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockUri.handle, blockUri.name, blockUri.version);
282
282
  const kapetaYmlPath = assetFile;
283
- if (!node_fs_1.default.existsSync(kapetaYmlPath)) {
283
+ if (!fs_extra_1.default.existsSync(kapetaYmlPath)) {
284
284
  throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
285
285
  }
286
286
  const spec = providerDefinition.definition.spec;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.33.5",
3
+ "version": "0.33.7",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -37,7 +37,7 @@ export interface EnrichedAsset {
37
37
  }
38
38
 
39
39
  function filterExists(asset: DefinitionInfo): boolean {
40
- return FS.existsSync(asset.path);
40
+ return FS.existsSync(asset.path) && FS.existsSync(asset.ymlPath);
41
41
  }
42
42
 
43
43
  function enrichAsset(asset: DefinitionInfo): EnrichedAsset {
@@ -87,6 +87,7 @@ export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
87
87
  const NANO_SECOND = 1000000;
88
88
  const HEALTH_CHECK_INTERVAL = 3000;
89
89
  const HEALTH_CHECK_MAX = 100;
90
+ const LATEST_PULL_TIMEOUT = 1000 * 60 * 15; // 15 minutes
90
91
  export const COMPOSE_LABEL_PROJECT = 'com.docker.compose.project';
91
92
  export const COMPOSE_LABEL_SERVICE = 'com.docker.compose.service';
92
93
 
@@ -135,12 +136,14 @@ class ContainerManager {
135
136
  private _version: string;
136
137
  private _lastDockerAccessCheck: number = 0;
137
138
  private logStreams: { [p: string]: { stream?: ClosableLogStream; timer?: NodeJS.Timeout } } = {};
139
+ private _latestImagePulls: { [p: string]: number } = {};
138
140
 
139
141
  constructor() {
140
142
  this._docker = null;
141
143
  this._alive = false;
142
144
  this._version = '';
143
145
  this._mountDir = Path.join(storageService.getKapetaBasedir(), 'mounts');
146
+ this._latestImagePulls = {};
144
147
  FSExtra.mkdirpSync(this._mountDir);
145
148
  }
146
149
 
@@ -321,7 +324,24 @@ class ContainerManager {
321
324
  .filter((imageData) => !!imageData.RepoTags)
322
325
  .map((imageData) => imageData.RepoTags as string[]);
323
326
 
324
- if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
327
+ const imageExists = imageTagList.some((imageTags) => imageTags.includes(image));
328
+
329
+ if (tag === 'latest') {
330
+ if (imageExists && this._latestImagePulls[imageName]) {
331
+ const lastPull = this._latestImagePulls[imageName];
332
+ const timeSinceLastPull = Date.now() - lastPull;
333
+ if (timeSinceLastPull < LATEST_PULL_TIMEOUT) {
334
+ console.log(
335
+ 'Image found and was pulled %s seconds ago: %s',
336
+ Math.round(timeSinceLastPull / 1000),
337
+ image
338
+ );
339
+ // Last pull was less than the timeout - don't pull again
340
+ return false;
341
+ }
342
+ }
343
+ this._latestImagePulls[imageName] = Date.now();
344
+ } else if (imageExists) {
325
345
  console.log('Image found: %s', image);
326
346
  return false;
327
347
  }
@@ -6,7 +6,7 @@
6
6
  import ClusterConfiguration, { DefinitionInfo } from '@kapeta/local-cluster-config';
7
7
  import { parseKapetaUri, normalizeKapetaUri, parseVersion } from '@kapeta/nodejs-utils';
8
8
  import { cacheManager, doCached } from './cacheManager';
9
- import { KapetaAPI } from '@kapeta/nodejs-api-client';
9
+ import { ExtendedIdentity, KapetaAPI } from '@kapeta/nodejs-api-client';
10
10
  import { Plan } from '@kapeta/schemas';
11
11
  import FS from 'fs-extra';
12
12
  import YAML from 'yaml';
@@ -58,12 +58,24 @@ class DefinitionsManager {
58
58
  // Not logged in yet, so we can't rewrite the sample plan
59
59
  return definitions;
60
60
  }
61
+
61
62
  const profile = await api.getCurrentIdentity();
62
63
  if (!profile) {
63
64
  // Not logged in yet, so we can't rewrite the sample plan
64
65
  return definitions;
65
66
  }
66
67
 
68
+ try {
69
+ await this.prepareSample(definitions, samplePlan, profile);
70
+ } catch (e) {
71
+ console.warn('Failed to prepare sample plan', e);
72
+ }
73
+
74
+ // Return the rewritten definitions
75
+ return ClusterConfiguration.getDefinitions();
76
+ }
77
+
78
+ private async prepareSample(definitions: DefinitionInfo[], samplePlan: DefinitionInfo, profile: ExtendedIdentity) {
67
79
  const newName = getRenamed(samplePlan, profile.handle);
68
80
 
69
81
  if (definitions.some((d) => d.definition.metadata.name === newName && d.version === 'local')) {
@@ -124,9 +136,6 @@ class DefinitionsManager {
124
136
  }
125
137
 
126
138
  console.log('Rewrite done for sample plan');
127
-
128
- // Return the rewritten definitions
129
- return ClusterConfiguration.getDefinitions();
130
139
  }
131
140
 
132
141
  private applyFilters(definitions: DefinitionInfo[], kindFilter: string[]): DefinitionInfo[] {
@@ -41,12 +41,16 @@ router.get('/:systemId/instances/:instanceId', (req: Request, res: Response) =>
41
41
  * Start all instances in a plan
42
42
  */
43
43
  router.post('/:systemId/start', async (req: Request, res: Response) => {
44
- const task = await instanceManager.startAllForPlan(req.params.systemId);
44
+ try {
45
+ const task = await instanceManager.startAllForPlan(req.params.systemId);
45
46
 
46
- res.status(202).send({
47
- ok: true,
48
- taskId: task.id,
49
- });
47
+ res.status(202).send({
48
+ ok: true,
49
+ taskId: task.id,
50
+ });
51
+ } catch (e: any) {
52
+ res.status(500).send({ ok: false, error: e.message });
53
+ }
50
54
  });
51
55
 
52
56
  /**
@@ -65,18 +69,22 @@ router.post('/:systemId/stop', async (req: Request, res: Response) => {
65
69
  * Start single instance in a plan
66
70
  */
67
71
  router.post('/:systemId/:instanceId/start', async (req: Request, res: Response) => {
68
- const result = await instanceManager.start(req.params.systemId, req.params.instanceId);
69
- if (result instanceof Task) {
70
- res.status(202).send({
71
- ok: true,
72
- taskId: result.id,
73
- });
74
- } else {
75
- res.status(202).send({
76
- ok: true,
77
- pid: result.pid,
78
- type: result.type,
79
- });
72
+ try {
73
+ const result = await instanceManager.start(req.params.systemId, req.params.instanceId);
74
+ if (result instanceof Task) {
75
+ res.status(202).send({
76
+ ok: true,
77
+ taskId: result.id,
78
+ });
79
+ } else {
80
+ res.status(202).send({
81
+ ok: true,
82
+ pid: result.pid,
83
+ type: result.type,
84
+ });
85
+ }
86
+ } catch (e: any) {
87
+ res.status(500).send({ ok: false, error: e.message });
80
88
  }
81
89
  });
82
90
 
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
 
6
- import FS from 'node:fs';
6
+ import FSExtra from 'fs-extra';
7
7
  import ClusterConfig, { DefinitionInfo } from '@kapeta/local-cluster-config';
8
8
  import { getBindHost, getBlockInstanceContainerName, readYML } from './utils';
9
9
  import { KapetaURI, parseKapetaUri, normalizeKapetaUri } from '@kapeta/nodejs-utils';
@@ -162,7 +162,7 @@ export class BlockInstanceRunner {
162
162
  ): Promise<ProcessInfo> {
163
163
  const baseDir = ClusterConfig.getRepositoryAssetPath(blockInfo.handle, blockInfo.name, blockInfo.version);
164
164
 
165
- if (!FS.existsSync(baseDir)) {
165
+ if (!FSExtra.existsSync(baseDir)) {
166
166
  throw new Error(
167
167
  `Local block not registered correctly - expected symlink here: ${baseDir}.\n` +
168
168
  `Make sure you've run "kap registry link" in your local directory to connect it to Kapeta`
@@ -226,7 +226,7 @@ export class BlockInstanceRunner {
226
226
  HealthCheck = containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
227
227
  }
228
228
 
229
- const realLocalPath = FS.realpathSync(baseDir);
229
+ const realLocalPath = await FSExtra.realpath(baseDir);
230
230
 
231
231
  const Mounts = containerManager.toDockerMounts({
232
232
  [workingDir]: toLocalBindVolume(realLocalPath),
@@ -293,7 +293,7 @@ export class BlockInstanceRunner {
293
293
  );
294
294
 
295
295
  const versionYml = versionFile;
296
- if (!FS.existsSync(versionYml)) {
296
+ if (!FSExtra.existsSync(versionYml)) {
297
297
  throw new Error(`Did not find version info at the expected path: ${versionYml}`);
298
298
  }
299
299
 
@@ -373,7 +373,7 @@ export class BlockInstanceRunner {
373
373
  );
374
374
 
375
375
  const kapetaYmlPath = assetFile;
376
- if (!FS.existsSync(kapetaYmlPath)) {
376
+ if (!FSExtra.existsSync(kapetaYmlPath)) {
377
377
  throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
378
378
  }
379
379