underpost 2.99.4 → 2.99.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.env.development +0 -3
  2. package/.env.production +1 -3
  3. package/.env.test +0 -3
  4. package/README.md +3 -3
  5. package/baremetal/commission-workflows.json +93 -4
  6. package/bin/deploy.js +56 -45
  7. package/cli.md +45 -28
  8. package/examples/static-page/README.md +101 -357
  9. package/examples/static-page/ssr-components/CustomPage.js +1 -13
  10. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +40 -0
  11. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +40 -0
  12. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  13. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  14. package/package.json +3 -4
  15. package/scripts/disk-devices.sh +13 -0
  16. package/scripts/maas-setup.sh +13 -9
  17. package/scripts/rocky-kickstart.sh +294 -0
  18. package/src/cli/baremetal.js +657 -263
  19. package/src/cli/cloud-init.js +120 -120
  20. package/src/cli/env.js +4 -1
  21. package/src/cli/image.js +4 -37
  22. package/src/cli/index.js +56 -11
  23. package/src/cli/kickstart.js +149 -0
  24. package/src/cli/repository.js +3 -1
  25. package/src/cli/run.js +56 -10
  26. package/src/cli/secrets.js +0 -34
  27. package/src/cli/static.js +23 -23
  28. package/src/client/components/core/Docs.js +22 -3
  29. package/src/index.js +30 -5
  30. package/src/server/backup.js +11 -4
  31. package/src/server/client-build-docs.js +1 -1
  32. package/src/server/conf.js +0 -22
  33. package/src/server/cron.js +339 -130
  34. package/src/server/dns.js +10 -0
  35. package/src/server/logger.js +22 -27
  36. package/src/server/tls.js +14 -14
  37. package/examples/static-page/QUICK-REFERENCE.md +0 -481
  38. package/examples/static-page/STATIC-GENERATOR-GUIDE.md +0 -757
@@ -4,7 +4,6 @@
4
4
  * @namespace UnderpostCron
5
5
  */
6
6
 
7
- import { Cmd } from './conf.js';
8
7
  import { loggerFactory } from './logger.js';
9
8
  import { shellExec } from './process.js';
10
9
  import fs from 'fs-extra';
@@ -12,68 +11,192 @@ import Underpost from '../index.js';
12
11
 
13
12
  const logger = loggerFactory(import.meta);
14
13
 
14
+ const volumeHostPath = '/home/dd';
15
+ const enginePath = '/home/dd/engine';
16
+ const cronVolumeName = 'underpost-cron-container-volume';
17
+
18
+ /**
19
+ * Generates a Kubernetes CronJob YAML manifest string.
20
+ *
21
+ * @param {Object} params - CronJob parameters
22
+ * @param {string} params.name - CronJob name (max 52 chars, sanitized to DNS subdomain)
23
+ * @param {string} params.expression - Cron schedule expression (e.g., '0 0 * * *')
24
+ * @param {string} params.deployList - Comma-separated deploy IDs for the cron CLI
25
+ * @param {string} params.jobList - Comma-separated job IDs (e.g., 'dns', 'backup')
26
+ * @param {string} [params.image] - Container image (defaults to underpost/underpost-engine:<version>)
27
+ * @param {string} [params.namespace='default'] - Kubernetes namespace
28
+ * @param {boolean} [params.git=false] - Pass --git flag to cron CLI
29
+ * @param {boolean} [params.dev=false] - Use local ./ base path instead of global underpost installation
30
+ * @param {string} [params.cmd] - Optional pre-script commands to run before cron execution
31
+ * @param {boolean} [params.suspend=false] - Whether the CronJob is suspended
32
+ * @param {boolean} [params.dryRun=false] - Pass --dry-run flag to the cron command inside the container
33
+ * @returns {string} Kubernetes CronJob YAML manifest
34
+ * @memberof UnderpostCron
35
+ */
36
+ const cronJobYamlFactory = ({
37
+ name,
38
+ expression,
39
+ deployList,
40
+ jobList,
41
+ image,
42
+ namespace = 'default',
43
+ git = false,
44
+ dev = false,
45
+ cmd,
46
+ suspend = false,
47
+ dryRun = false,
48
+ }) => {
49
+ const containerImage = image || `underpost/underpost-engine:${Underpost.version}`;
50
+
51
+ const sanitizedName = name
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9-]/g, '-')
54
+ .replace(/--+/g, '-')
55
+ .replace(/^-|-$/g, '')
56
+ .substring(0, 52);
57
+
58
+ const cmdPart = cmd ? `${cmd} && ` : '';
59
+ const cronBin = dev ? 'node bin' : 'underpost';
60
+ const flags = `${git ? '--git ' : ''}${dev ? '--dev ' : ''}${dryRun ? '--dry-run ' : ''}`;
61
+ const cronCommand = `${cmdPart}${cronBin} cron ${flags}${deployList} ${jobList}`;
62
+
63
+ return `apiVersion: batch/v1
64
+ kind: CronJob
65
+ metadata:
66
+ name: ${sanitizedName}
67
+ namespace: ${namespace}
68
+ labels:
69
+ app: ${sanitizedName}
70
+ managed-by: underpost
71
+ spec:
72
+ schedule: "${expression}"
73
+ concurrencyPolicy: Forbid
74
+ startingDeadlineSeconds: 200
75
+ successfulJobsHistoryLimit: 3
76
+ failedJobsHistoryLimit: 1
77
+ suspend: ${suspend}
78
+ jobTemplate:
79
+ spec:
80
+ template:
81
+ metadata:
82
+ labels:
83
+ app: ${sanitizedName}
84
+ managed-by: underpost
85
+ spec:
86
+ containers:
87
+ - name: ${sanitizedName}
88
+ image: ${containerImage}
89
+ command:
90
+ - /bin/sh
91
+ - -c
92
+ - >
93
+ ${cronCommand}
94
+ volumeMounts:
95
+ - mountPath: ${enginePath}
96
+ name: ${cronVolumeName}
97
+ volumes:
98
+ - hostPath:
99
+ path: ${enginePath}
100
+ type: Directory
101
+ name: ${cronVolumeName}
102
+ restartPolicy: OnFailure
103
+ `;
104
+ };
105
+
106
+ /**
107
+ * Syncs the engine directory into the kind-worker container node.
108
+ * Required for kind clusters where worker nodes don't share the host filesystem.
109
+ *
110
+ * @memberof UnderpostCron
111
+ */
112
+ const syncEngineToKindWorker = () => {
113
+ logger.info('Syncing engine volume to kind-worker node');
114
+ shellExec(`docker exec -i kind-worker bash -c "rm -rf ${volumeHostPath}"`);
115
+ shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${volumeHostPath}"`);
116
+ shellExec(`docker cp ${volumeHostPath}/engine kind-worker:${volumeHostPath}/engine`);
117
+ shellExec(
118
+ `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${volumeHostPath}; chmod -R 755 ${volumeHostPath}"`,
119
+ );
120
+ };
121
+
122
+ /**
123
+ * Resolves the deploy-id to use for cron job generation.
124
+ * When deployId is provided directly, uses it. Otherwise reads from dd.cron file.
125
+ *
126
+ * @param {string} [deployId] - Explicit deploy-id override
127
+ * @memberof UnderpostCron
128
+ * @returns {string|null} Resolved deploy-id or null if not found
129
+ */
130
+ const resolveDeployId = (deployId) => {
131
+ if (deployId) return deployId;
132
+
133
+ const cronDeployFilePath = './engine-private/deploy/dd.cron';
134
+ if (!fs.existsSync(cronDeployFilePath)) {
135
+ return null;
136
+ }
137
+ return fs.readFileSync(cronDeployFilePath, 'utf8').trim();
138
+ };
139
+
15
140
  /**
16
141
  * UnderpostCron main module methods
17
142
  * @class UnderpostCron
18
143
  * @memberof UnderpostCron
19
144
  */
20
145
  class UnderpostCron {
21
- /**
22
- * Get the JOB static member
23
- * @static
24
- * @type {Object}
25
- * @memberof UnderpostCron
26
- */
146
+ /** @returns {Object} Available cron job handlers */
27
147
  static get JOB() {
28
148
  return {
29
- /**
30
- * DNS cli API
31
- * @static
32
- * @type {Dns}
33
- * @memberof UnderpostCron
34
- */
35
149
  dns: Underpost.dns,
36
- /**
37
- * BackUp cli API
38
- * @static
39
- * @type {BackUp}
40
- * @memberof UnderpostCron
41
- */
42
150
  backup: Underpost.backup,
43
151
  };
44
152
  }
45
153
 
46
154
  static API = {
47
155
  /**
48
- * Run the cron jobs
49
- * @static
50
- * @param {String} deployList - Comma separated deploy ids
51
- * @param {String} jobList - Comma separated job ids
52
- * @param {Object} options - Options for cron execution
53
- * @return {void}
156
+ * CLI entry point for the `underpost cron` command.
157
+ *
158
+ * @param {string} deployList - Comma-separated deploy IDs
159
+ * @param {string} jobList - Comma-separated job IDs
160
+ * @param {Object} options - CLI flags
161
+ * @param {boolean} [options.generateK8sCronjobs] - Generate K8s CronJob YAML manifests
162
+ * @param {boolean} [options.apply] - Apply manifests to the cluster
163
+ * @param {boolean} [options.git] - Pass --git to job execution
164
+ * @param {boolean} [options.dev] - Use local ./ base path instead of global underpost installation
165
+ * @param {string} [options.cmd] - Optional pre-script commands to run before cron execution
166
+ * @param {string} [options.namespace] - Kubernetes namespace
167
+ * @param {string} [options.image] - Custom container image
168
+ * @param {string} [options.setupStart] - Deploy-id to setup: updates its package.json start and generates+applies cron jobs
169
+ * @param {boolean} [options.k3s] - Use k3s cluster context (apply directly on host)
170
+ * @param {boolean} [options.kind] - Use kind cluster context (apply via kind-worker container)
171
+ * @param {boolean} [options.kubeadm] - Use kubeadm cluster context (apply directly on host)
172
+ * @param {boolean} [options.dryRun] - Preview cron jobs without executing them
173
+ * @param {boolean} [options.createJobNow] - After applying, immediately create a Job from each CronJob (requires --apply)
54
174
  * @memberof UnderpostCron
55
175
  */
56
176
  callback: async function (
57
177
  deployList = 'default',
58
178
  jobList = Object.keys(Underpost.cron.JOB).join(','),
59
- options = { initPm2Cronjobs: false, git: false, updatePackageScripts: false },
179
+ options = {},
60
180
  ) {
61
- if (options.updatePackageScripts === true) {
62
- await Underpost.cron.updatePackageScripts(deployList);
181
+ if (options.setupStart) {
182
+ await Underpost.cron.setupDeployStart(options.setupStart, options);
63
183
  return;
64
184
  }
65
185
 
66
- if (options.initPm2Cronjobs === true) {
67
- await Underpost.cron.initCronJobs(options);
186
+ if (options.generateK8sCronjobs) {
187
+ await Underpost.cron.generateK8sCronJobs(options);
68
188
  return;
69
189
  }
70
190
 
71
- // Execute the requested jobs
72
191
  for (const _jobId of jobList.split(',')) {
73
192
  const jobId = _jobId.trim();
74
193
  if (Underpost.cron.JOB[jobId]) {
75
- logger.info(`Executing cron job: ${jobId}`);
76
- await Underpost.cron.JOB[jobId].callback(deployList, options);
194
+ if (options.dryRun) {
195
+ logger.info(`[dry-run] Would execute cron job`, { jobId, deployList, options });
196
+ } else {
197
+ logger.info(`Executing cron job`, { jobId, deployList, options });
198
+ await Underpost.cron.JOB[jobId].callback(deployList, options);
199
+ }
77
200
  } else {
78
201
  logger.warn(`Unknown cron job: ${jobId}`);
79
202
  }
@@ -81,140 +204,225 @@ class UnderpostCron {
81
204
  },
82
205
 
83
206
  /**
84
- * Initialize PM2 cron jobs from configuration
85
- * @static
86
- * @param {Object} options - Initialization options
207
+ * Update the package.json start script for the given deploy-id and generate+apply its K8s CronJob manifests.
208
+ *
209
+ * @param {string} deployId - The deploy-id whose package.json will be updated
210
+ * @param {Object} [options] - Additional options forwarded to generateK8sCronJobs
211
+ * @param {boolean} [options.createJobNow] - After applying, immediately create a Job from each CronJob
212
+ * @param {boolean} [options.dryRun] - Pass --dry-run=client to kubectl commands
213
+ * @param {boolean} [options.apply] - Whether to apply generated manifests to the cluster
214
+ * @param {boolean} [options.git] - Pass --git flag to cron CLI commands
215
+ * @param {boolean} [options.dev] - Use local ./ base path instead of global underpost installation
216
+ * @param {string} [options.cmd] - Optional pre-script commands to run before cron execution
217
+ * @param {string} [options.namespace] - Kubernetes namespace for the CronJobs
218
+ * @param {string} [options.image] - Custom container image override for the CronJobs
219
+ * @param {boolean} [options.k3s] - k3s cluster context (apply directly on host)
220
+ * @param {boolean} [options.kind] - kind cluster context (apply via kind-worker container)
221
+ * @param {boolean} [options.kubeadm] - kubeadm cluster context (apply directly on host)
87
222
  * @memberof UnderpostCron
88
223
  */
89
- initCronJobs: async function (options = { git: false }) {
90
- logger.info('Initializing PM2 cron jobs');
91
-
92
- // Read cron job deployment ID from dd.cron file (e.g., "dd-cron")
93
- const jobDeployId = fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
94
- const confCronPath = `./engine-private/conf/${jobDeployId}/conf.cron.json`;
224
+ setupDeployStart: async function (deployId, options = {}) {
225
+ if (!deployId || deployId === true) deployId = resolveDeployId();
226
+ const confDir = `./engine-private/conf/${deployId}`;
227
+ const packageJsonPath = `${confDir}/package.json`;
228
+ const confCronPath = `${confDir}/conf.cron.json`;
95
229
 
96
230
  if (!fs.existsSync(confCronPath)) {
97
- logger.warn(`Cron configuration not found: ${confCronPath}`);
231
+ logger.warn(`conf.cron.json not found for deploy-id: ${deployId}`, { path: confCronPath });
98
232
  return;
99
233
  }
100
234
 
101
- const confCronConfig = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
235
+ const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
102
236
 
103
- if (!confCronConfig.jobs || Object.keys(confCronConfig.jobs).length === 0) {
104
- logger.info('No cron jobs configured');
237
+ if (!confCron.jobs || Object.keys(confCron.jobs).length === 0) {
238
+ logger.warn(`No cron jobs configured for deploy-id: ${deployId}`);
105
239
  return;
106
240
  }
107
241
 
108
- // Delete all existing cron jobs
109
- for (const job of Object.keys(confCronConfig.jobs)) {
110
- const name = `${jobDeployId}-${job}`;
111
- logger.info(`Removing existing PM2 process: ${name}`);
112
- shellExec(Cmd.delete(name));
242
+ const hasEnabledJobs = Object.values(confCron.jobs).some((job) => job.enabled !== false);
243
+ if (!hasEnabledJobs) {
244
+ logger.warn(`No enabled cron jobs for deploy-id: ${deployId}`);
245
+ return;
113
246
  }
114
247
 
115
- // Create PM2 cron jobs for each configured job
116
- for (const job of Object.keys(confCronConfig.jobs)) {
117
- const jobConfig = confCronConfig.jobs[job];
118
-
119
- if (jobConfig.enabled === false) {
120
- logger.info(`Skipping disabled job: ${job}`);
121
- continue;
122
- }
123
-
124
- const name = `${jobDeployId}-${job}`;
125
- const deployIdList = Underpost.cron.getRelatedDeployIdList(job);
126
- const expression = jobConfig.expression || '0 0 * * *'; // Default: daily at midnight
127
- const instances = jobConfig.instances || 1; // Default: 1 instance
128
-
129
- logger.info(`Creating PM2 cron job: ${name} with expression: ${expression}, instances: ${instances}`);
130
- shellExec(Cmd.cron(deployIdList, job, name, expression, options, instances));
248
+ // Update package.json start script
249
+ if (fs.existsSync(packageJsonPath)) {
250
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
251
+ let startCommand = 'echo "Starting cron jobs..."';
252
+ for (const job of Object.keys(confCron.jobs))
253
+ startCommand += ` && kubectl apply -f ./manifests/cronjobs/${deployId}/${deployId}-${job}.yaml`;
254
+ if (!packageJson.scripts) packageJson.scripts = {};
255
+ packageJson.scripts.start = startCommand;
256
+
257
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n', 'utf8');
258
+ logger.info(`Updated package.json start script for ${deployId}`, { path: packageJsonPath });
259
+ } else {
260
+ logger.warn(`package.json not found for deploy-id: ${deployId}`, { path: packageJsonPath });
131
261
  }
132
262
 
133
- logger.info('PM2 cron jobs initialization completed');
263
+ // Generate and apply cron job manifests for this deploy-id
264
+ await Underpost.cron.generateK8sCronJobs({
265
+ deployId,
266
+ apply: options.apply,
267
+ git: !!options.git,
268
+ dev: !!options.dev,
269
+ cmd: options.cmd,
270
+ namespace: options.namespace,
271
+ image: options.image,
272
+ k3s: !!options.k3s,
273
+ kind: !!options.kind,
274
+ kubeadm: !!options.kubeadm,
275
+ createJobNow: !!options.createJobNow,
276
+ dryRun: !!options.dryRun,
277
+ });
134
278
  },
135
279
 
136
280
  /**
137
- * Update package.json start scripts for specified deploy-ids
138
- * @static
139
- * @param {String} deployList - Comma separated deploy ids
281
+ * Generate Kubernetes CronJob YAML manifests from conf.cron.json configuration.
282
+ * Each enabled job produces one CronJob YAML file under manifests/cronjobs/<deployId>/.
283
+ * With --apply the manifests are also applied to the cluster via kubectl.
284
+ *
285
+ * @param {Object} options
286
+ * @param {string} [options.deployId] - Explicit deploy-id (overrides dd.cron file lookup)
287
+ * @param {boolean} [options.git=false] - Pass --git flag to cron CLI commands
288
+ * @param {boolean} [options.dev=false] - Use local ./ base path instead of global underpost
289
+ * @param {string} [options.cmd] - Optional pre-script commands
290
+ * @param {boolean} [options.apply=false] - kubectl apply generated manifests
291
+ * @param {string} [options.namespace='default'] - Target Kubernetes namespace
292
+ * @param {string} [options.image] - Custom container image override
293
+ * @param {boolean} [options.k3s=false] - k3s cluster context (apply directly on host)
294
+ * @param {boolean} [options.kind=false] - kind cluster context (apply via kind-worker container)
295
+ * @param {boolean} [options.kubeadm=false] - kubeadm cluster context (apply directly on host)
296
+ * @param {boolean} [options.createJobNow=false] - After applying, create a Job from each CronJob immediately
297
+ * @param {boolean} [options.dryRun=false] - Pass --dry-run=client to kubectl commands
140
298
  * @memberof UnderpostCron
141
299
  */
142
- updatePackageScripts: async function (deployList = 'default') {
143
- logger.info('Updating package.json start scripts for deploy-id configurations');
300
+ generateK8sCronJobs: async function (options = {}) {
301
+ const namespace = options.namespace || 'default';
302
+ const jobDeployId = resolveDeployId(options.deployId);
303
+
304
+ if (!jobDeployId) {
305
+ logger.warn(
306
+ 'Could not resolve deploy-id. Provide --setup-start <deploy-id> or create engine-private/deploy/dd.cron',
307
+ );
308
+ return;
309
+ }
310
+
311
+ const confCronPath = `./engine-private/conf/${jobDeployId}/conf.cron.json`;
144
312
 
145
- // Resolve deploy list
146
- if ((!deployList || deployList === 'dd') && fs.existsSync(`./engine-private/deploy/dd.router`)) {
147
- deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim();
313
+ if (!fs.existsSync(confCronPath)) {
314
+ logger.warn(`Cron configuration not found: ${confCronPath}`);
315
+ return;
148
316
  }
149
317
 
150
- const confDir = './engine-private/conf';
151
- if (!fs.existsSync(confDir)) {
152
- logger.warn(`Configuration directory not found: ${confDir}`);
318
+ const confCronConfig = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
319
+
320
+ if (!confCronConfig.jobs || Object.keys(confCronConfig.jobs).length === 0) {
321
+ logger.info('No cron jobs configured');
153
322
  return;
154
323
  }
155
324
 
156
- // Parse deploy list into array
157
- const deployIds = deployList
158
- .split(',')
159
- .map((id) => id.trim())
160
- .filter((id) => id);
325
+ const outputDir = `./manifests/cronjobs/${jobDeployId}`;
326
+ fs.mkdirSync(outputDir, { recursive: true });
161
327
 
162
- for (const deployId of deployIds) {
163
- const packageJsonPath = `${confDir}/${deployId}/package.json`;
164
- const confCronPath = `${confDir}/${deployId}/conf.cron.json`;
328
+ const generatedFiles = [];
165
329
 
166
- // Only update if both package.json and conf.cron.json exist
167
- if (!fs.existsSync(packageJsonPath)) {
168
- logger.info(`Skipping ${deployId}: package.json not found`);
169
- continue;
170
- }
330
+ for (const job of Object.keys(confCronConfig.jobs)) {
331
+ const jobConfig = confCronConfig.jobs[job];
171
332
 
172
- if (!fs.existsSync(confCronPath)) {
173
- logger.info(`Skipping ${deployId}: conf.cron.json not found`);
333
+ if (jobConfig.enabled === false) {
334
+ logger.info(`Skipping disabled job: ${job}`);
174
335
  continue;
175
336
  }
176
337
 
177
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
178
- const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
179
-
180
- // Build start script based on cron jobs configuration
181
- if (confCron.jobs && Object.keys(confCron.jobs).length > 0) {
182
- const hasEnabledJobs = Object.values(confCron.jobs).some((job) => job.enabled !== false);
338
+ const deployIdList = Underpost.cron.getRelatedDeployIdList(job);
339
+ const expression = jobConfig.expression || '0 0 * * *';
340
+ const cronJobName = `${jobDeployId}-${job}`;
341
+
342
+ const yamlContent = cronJobYamlFactory({
343
+ name: cronJobName,
344
+ expression,
345
+ deployList: deployIdList,
346
+ jobList: job,
347
+ image: options.image,
348
+ namespace,
349
+ git: !!options.git,
350
+ dev: !!options.dev,
351
+ cmd: options.cmd,
352
+ suspend: false,
353
+ dryRun: !!options.dryRun,
354
+ });
355
+
356
+ const yamlFilePath = `${outputDir}/${cronJobName}.yaml`;
357
+ fs.writeFileSync(yamlFilePath, yamlContent, 'utf8');
358
+ generatedFiles.push(yamlFilePath);
359
+
360
+ logger.info(`Generated CronJob manifest: ${yamlFilePath}`, { job, expression, namespace });
361
+ }
183
362
 
184
- if (hasEnabledJobs) {
185
- // Update start script with PM2 cron jobs initialization
186
- const startScript = 'pm2 flush && pm2 reloadLogs && node bin cron --init-pm2-cronjobs --git';
363
+ if (options.apply) {
364
+ // Delete existing CronJobs before applying new ones
365
+ for (const job of Object.keys(confCronConfig.jobs)) {
366
+ const cronJobName = `${jobDeployId}-${job}`;
367
+ shellExec(`kubectl delete cronjob ${cronJobName} --namespace=${namespace} --ignore-not-found`);
368
+ }
187
369
 
188
- if (!packageJson.scripts) {
189
- packageJson.scripts = {};
190
- }
370
+ // Ensure default dockerhub image is loaded on the cluster when no custom image is provided
371
+ if (!options.image) {
372
+ logger.info('Ensuring default image is loaded on cluster');
373
+ Underpost.image.pullDockerHubImage({
374
+ dockerhubImage: 'underpost',
375
+ kind: !!options.kind,
376
+ k3s: !!options.k3s,
377
+ kubeadm: !!options.kubeadm,
378
+ dev: !!options.dev,
379
+ });
380
+ }
191
381
 
192
- packageJson.scripts.start = startScript;
382
+ // Sync engine volume to kind-worker node if using kind cluster
383
+ if (options.kind) {
384
+ syncEngineToKindWorker();
385
+ }
193
386
 
194
- // Write updated package.json
195
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n', 'utf8');
196
- logger.info(`Updated package.json for ${deployId} with cron start script`);
197
- } else {
198
- logger.info(`Skipping ${deployId}: no enabled cron jobs`);
387
+ for (const yamlFile of generatedFiles) {
388
+ logger.info(`Applying: ${yamlFile}`);
389
+ shellExec(`kubectl apply -f ${yamlFile}`);
390
+ }
391
+ logger.info('All CronJob manifests applied');
392
+
393
+ // Create an immediate Job from each CronJob if requested
394
+ if (options.createJobNow) {
395
+ for (const job of Object.keys(confCronConfig.jobs)) {
396
+ const jobConfig = confCronConfig.jobs[job];
397
+ if (jobConfig.enabled === false) continue;
398
+
399
+ const cronJobName = `${jobDeployId}-${job}`
400
+ .toLowerCase()
401
+ .replace(/[^a-z0-9-]/g, '-')
402
+ .replace(/--+/g, '-')
403
+ .replace(/^-|-$/g, '')
404
+ .substring(0, 52);
405
+
406
+ const immediateJobName = `${cronJobName}-now-${Date.now()}`.substring(0, 63);
407
+ logger.info(`Creating immediate Job from CronJob: ${cronJobName}`, { jobName: immediateJobName });
408
+ shellExec(`kubectl create job ${immediateJobName} --from=cronjob/${cronJobName} -n ${namespace}`);
199
409
  }
200
- } else {
201
- logger.info(`Skipping ${deployId}: no cron jobs configured`);
410
+ logger.info('All immediate Jobs created');
202
411
  }
412
+ } else {
413
+ logger.info(`Manifests generated in ${outputDir}. Use --apply to deploy to the cluster.`);
203
414
  }
204
-
205
- logger.info('Package.json start scripts update completed');
206
415
  },
207
416
 
208
417
  /**
209
- * Get the related deploy id list for the given job id
210
- * @static
211
- * @param {String} jobId - The job id (e.g., 'dns', 'backup')
212
- * @return {String} Comma-separated list of deploy ids to process
418
+ * Resolve the deploy-id list associated with a given job.
419
+ * Backup jobs read from dd.router (multiple deploy-ids); others from dd.cron.
420
+ *
421
+ * @param {string} jobId - Job identifier (e.g., 'dns', 'backup')
422
+ * @returns {string} Comma-separated deploy IDs
213
423
  * @memberof UnderpostCron
214
424
  */
215
425
  getRelatedDeployIdList(jobId) {
216
- // Backup job uses dd.router file (contains multiple deploy-ids)
217
- // Other jobs use dd.cron file (contains single deploy-id)
218
426
  const deployFilePath =
219
427
  jobId === 'backup' ? './engine-private/deploy/dd.router' : './engine-private/deploy/dd.cron';
220
428
 
@@ -225,30 +433,31 @@ class UnderpostCron {
225
433
  : 'dd-cron';
226
434
  }
227
435
 
228
- // Return the deploy-id list from the file (may be single or comma-separated)
229
436
  return fs.readFileSync(deployFilePath, 'utf8').trim();
230
437
  },
231
438
 
232
439
  /**
233
- * Get the JOB static object
234
- * @static
235
- * @type {Object}
440
+ * Get the available cron job handlers.
441
+ * Each handler should have a callback function that executes the job logic.
236
442
  * @memberof UnderpostCron
443
+ * @returns {Object} Available cron job handlers
237
444
  */
238
445
  get JOB() {
239
446
  return UnderpostCron.JOB;
240
447
  },
241
448
 
242
449
  /**
243
- * Get the list of available job IDs
244
- * @static
245
- * @return {Array<String>} List of job IDs
450
+ * Get the list of available job IDs.
451
+ * This is derived from the keys of the JOB object.
246
452
  * @memberof UnderpostCron
453
+ * @returns {string[]} List of available job IDs
247
454
  */
248
- getJobsIDs: function () {
455
+ getJobsIDs() {
249
456
  return Object.keys(UnderpostCron.JOB);
250
457
  },
251
458
  };
252
459
  }
253
460
 
254
461
  export default UnderpostCron;
462
+
463
+ export { cronJobYamlFactory, resolveDeployId };
package/src/server/dns.js CHANGED
@@ -13,6 +13,8 @@ import dns from 'node:dns';
13
13
  import os from 'node:os';
14
14
  import { shellExec, pbcopy } from './process.js';
15
15
  import Underpost from '../index.js';
16
+ import { writeEnv } from './conf.js';
17
+ import { resolveDeployId } from './cron.js';
16
18
 
17
19
  dotenv.config();
18
20
 
@@ -328,6 +330,14 @@ class Dns {
328
330
  logger.info('IP updated successfully and verified', testIp);
329
331
  Underpost.env.set('ip', testIp);
330
332
  Underpost.env.delete('monitor-input');
333
+ {
334
+ const deployId = resolveDeployId();
335
+ const envs = dotenv.parse(
336
+ fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}`, 'utf8'),
337
+ );
338
+ envs.ip = testIp;
339
+ writeEnv(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}`, envs);
340
+ }
331
341
  } else {
332
342
  logger.error('IP not updated or verification failed', { expected: testIp, received: verifyIp });
333
343
  }