From 57c7405055849aa2786fb8bbebff4b4f7824a22a Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Wed, 1 Oct 2025 16:58:39 +0200 Subject: [PATCH 01/14] development profile support Signed-off-by: marcopiraccini --- .env.sample | 6 +-- README.md | 20 ++++++++ cli/cluster.js | 2 + lib/cluster/k3d.js | 22 ++++++-- lib/context.js | 2 +- lib/platformatic.js | 102 ++++++++++++++++++++++++++++++++++++-- profiles/development.yaml | 82 ++++++++++++++++++++++++++++++ schemas/v4/profile.js | 24 +++------ 8 files changed, 230 insertions(+), 30 deletions(-) create mode 100644 profiles/development.yaml diff --git a/.env.sample b/.env.sample index e6c835f..180d116 100644 --- a/.env.sample +++ b/.env.sample @@ -3,6 +3,6 @@ GITHUB_OAUTH_CLIENT_ID= GITHUB_OAUTH_CLIENT_SECRET= GITHUB_OAUTH_VALID_EMAILS= -# Use a Docker PAT, Docker OAT, or Github PAT -PULL_SECRET_USER= -PULL_SECRET_TOKEN= +# Use a Docker PAT, Docker OAT, or Github PAT IF NECESSARY +# PULL_SECRET_USER= +# PULL_SECRET_TOKEN= diff --git a/README.md b/README.md index f17b9b1..4a93906 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,26 @@ to: ## Examples +### Development Profile + +The `development` profile enables hot reloading for ICC and Machinist services using local repositories: + +```sh +export ICC_REPO=/path/to/icc3 +export MACHINIST_REPO=/path/to/machinist +desk cluster up --profile development +``` + +This profile: +- Mounts your local ICC and Machinist repositories into the Kubernetes cluster +- Runs services with `pnpm run dev` for hot reloading +- Sets `DEV_K8S=true` to enable Platformatic DB service file watching +- Uses the same base image (`node:22.20.0-alpine`) as production for native module compatibility + +When code changes are made in the local repositories, the services will automatically reload. + +### Testing ICC Installation Script + Test out the installation script from ICC: ```sh diff --git a/cli/cluster.js b/cli/cluster.js index a42a2a4..d44479e 100644 --- a/cli/cluster.js +++ b/cli/cluster.js @@ -47,6 +47,8 @@ export default async function cli (argv) { const infra = await platformatic.createChartConfig(context.platformatic, { context }) await installInfra(infra, { context }) + await platformatic.patchForDevMode(context.platformatic, { context }) + info('Waiting for Platformatic to finish starting') const k8sContext = { namespace: infra[platformatic.CHART_NAME].namespace diff --git a/lib/cluster/k3d.js b/lib/cluster/k3d.js index 96d3159..6722867 100644 --- a/lib/cluster/k3d.js +++ b/lib/cluster/k3d.js @@ -26,13 +26,29 @@ export async function startCluster ({ provider, chartDir, name, platformatic }) '--wait' ] - const volumes = Object.entries(platformatic) - .filter(([, config]) => config.hotReload && config.local?.path) - .map(([name, config]) => `--volume=${config.local.path}:/data/local/${name}@server:0`) + const volumes = [] + let needsNodeImage = false + if (platformatic.services) { + Object.entries(platformatic.services) + .filter(([, config]) => config.hotReload && config.localRepo) + .forEach(([name, config]) => { + volumes.push(`--volume=${config.localRepo}:/data/local/${name}@server:0`) + needsNodeImage = true + }) + } args = args.concat(volumes) await spawn('k3d', args) + + if (needsNodeImage) { + try { + await spawn('docker', ['pull', 'node:22.20.0-alpine']) + await spawn('k3d', ['image', 'import', 'node:22.20.0-alpine', '-c', clusterName(name)]) + } catch (err) { + // Image might already be imported, continue + } + } } export async function stopCluster ({ name }) { diff --git a/lib/context.js b/lib/context.js index 62a1aa5..48c943b 100644 --- a/lib/context.js +++ b/lib/context.js @@ -8,7 +8,7 @@ import { parseConfig } from '../schemas/config.js' import { CHART_NAME as PLT_CHART_NAME } from './platformatic.js' const deepmerge = Deepmerge() -const userSecrets = {} +const userSecrets = { ...process.env } dotenv.config({ quiet: true, processEnv: userSecrets }) export async function loadContext (profileNameOrPath) { diff --git a/lib/platformatic.js b/lib/platformatic.js index d67f5f9..7a135c0 100644 --- a/lib/platformatic.js +++ b/lib/platformatic.js @@ -1,7 +1,7 @@ import { join } from 'node:path' import Deepmerge from '@fastify/deepmerge' import { getClusterStatus } from './cluster/index.js' -import { loadYamlFile } from './utils.js' +import { loadYamlFile, spawn } from './utils.js' const deepmerge = Deepmerge() @@ -18,10 +18,10 @@ export async function createChartConfig (values, { context }) { VALKEY_ICC_CONNECTION_STRING: clusterStatus.valkey.connectionString, PROMETHEUS_URL: clusterStatus.prometheus.url, PUBLIC_URL: 'https://icc.plt', - ICC_IMAGE_REPO: apps.icc.image.repository, - ICC_IMAGE_TAG: apps.icc.image.tag, - MACHINIST_IMAGE_REPO: apps.machinist.image.repository, - MACHINIST_IMAGE_TAG: apps.machinist.image.tag, + ICC_IMAGE_REPO: apps.icc.image?.repository || 'platformatic/intelligent-command-center', + ICC_IMAGE_TAG: apps.icc.image?.tag || 'latest', + MACHINIST_IMAGE_REPO: apps.machinist.image?.repository || 'platformatic/machinist', + MACHINIST_IMAGE_TAG: apps.machinist.image?.tag || 'latest', PLT_NAMESPACES: context.cluster.namespaces } const overrideValues = await loadYamlFile(join(context.chartDir, CHART_NAME, 'overrides.yaml'), substitutions) @@ -29,7 +29,99 @@ export async function createChartConfig (values, { context }) { delete deskValues.chart const overrides = deepmerge(overrideValues, deskValues) + + if (apps.icc.hotReload && apps.icc.localRepo) { + overrides.services.icc.image = { + repository: 'node', + tag: '22.20.0-alpine', + pullPolicy: 'IfNotPresent' + } + overrides.services.icc.log_level = 'debug' + } + + if (apps.machinist.hotReload && apps.machinist.localRepo) { + overrides.services.machinist.image = { + repository: 'node', + tag: '22.20.0-alpine', + pullPolicy: 'IfNotPresent' + } + overrides.services.machinist.log_level = 'debug' + } + const chartConfig = { ...values.chart, overrides } return { [CHART_NAME]: chartConfig } } + +export async function patchForDevMode (values, { context }) { + const { apps } = values + const namespace = 'platformatic' + + if (apps.icc.hotReload && apps.icc.localRepo) { + await patchDeployment('icc', { + volumes: [{ + name: 'icc-local-repo', + hostPath: { + path: '/data/local/icc', + type: 'Directory' + } + }], + volumeMounts: [{ + name: 'icc-local-repo', + mountPath: '/app' + }], + command: ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] + }, namespace, context) + } + + if (apps.machinist.hotReload && apps.machinist.localRepo) { + await patchDeployment('machinist', { + volumes: [{ + name: 'machinist-local-repo', + hostPath: { + path: '/data/local/machinist', + type: 'Directory' + } + }], + volumeMounts: [{ + name: 'machinist-local-repo', + mountPath: '/app' + }], + command: ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] + }, namespace, context) + } +} + +async function patchDeployment (name, config, namespace, context) { + const patch = { + spec: { + template: { + spec: { + volumes: config.volumes, + containers: [{ + name, + volumeMounts: config.volumeMounts, + command: config.command, + workingDir: '/app', + env: [ + { + name: 'DEV_K8S', + value: 'true' + } + ] + }] + } + } + } + } + + await spawn('kubectl', [ + '--context', context.kube.contextName, + '--namespace', namespace, + 'patch', + 'deployment', + name, + '--type', 'strategic', + '--patch', JSON.stringify(patch) + ]) +} diff --git a/profiles/development.yaml b/profiles/development.yaml new file mode 100644 index 0000000..20f899e --- /dev/null +++ b/profiles/development.yaml @@ -0,0 +1,82 @@ +version: 4 + +description: | + This setup creates special images for icc and machinist which have volume mounts + mapped to local repositories. Essentially the same as running `cib dev` + +cluster: + namespaces: + - platformatic + + k3d: + nodes: 1 + +dependencies: + prometheus-community/prometheus-adapter: + plt_defaults: true + prometheus-community/kube-prometheus-stack: + plt_defaults: true + cloudpirates/postgres: + plt_defaults: true + cloudpirates/valkey: + plt_defaults: true + local/traefik: + plt_defaults: false + +platformatic: + imagePullSecret: + registry: docker.io + user: "{{ PULL_SECRET_USER }}" + token: "{{ PULL_SECRET_TOKEN }}" + + services: + icc: + hotReload: true + localRepo: "{{ ICC_REPO }}" + + image: + tag: "dev" + repository: "platformatic/intelligent-command-center" + + features: + cache_recommendations: + enable: false + risk_service_dump: + enable: false + ffc: + enable: false + icc_jobs: + enable: true + + login_methods: + google: + enable: false + client_id: "" + client_secret: "" + valid_emails: "" + github: + enable: true + client_id: "{{ GITHUB_OAUTH_CLIENT_ID }}" + client_secret: "{{ GITHUB_OAUTH_CLIENT_SECRET }}" + valid_emails: "{{ GITHUB_OAUTH_VALID_EMAILS }}" + + log_level: debug + + secrets: + icc_session: "nqS4bQDlFNZfd1PtLwbkCDEgJiozzxRuyslNPtSSdeQ=" + control_plane_keys: "iUIz122f2Kh49Q2PvGxJWuajJRm8B0TZ7orfGbf29LA=" + user_manager_session: "XnlPIbATw2x/7xIX304esO9qKuCZLR3HOa8wTF6O3pc=" + + machinist: + hotReload: true + localRepo: "{{ MACHINIST_REPO }}" + + image: + tag: "dev" + repository: "platformatic/machinist" + + features: + event_export: + enable: false + + log_level: debug diff --git a/schemas/v4/profile.js b/schemas/v4/profile.js index 4c1a87f..167c0bb 100644 --- a/schemas/v4/profile.js +++ b/schemas/v4/profile.js @@ -6,21 +6,14 @@ const InfraComponentSchema = Type.Object({ plt_defaults: Type.Boolean() }) -// Schema for platformatic service configuration with image -const PlatformaticServiceWithImageSchema = Type.Object({ - image: Type.Object({ +// Schema for platformatic service configuration +const PlatformaticServiceSchema = Type.Object({ + hotReload: Type.Optional(Type.Boolean()), + localRepo: Type.Optional(Type.String()), + image: Type.Optional(Type.Object({ tag: Type.String(), repository: Type.String() - }) -}, { -}) - -// Schema for platformatic service configuration with local -const PlatformaticServiceWithLocalSchema = Type.Object({ - hotReload: Type.Optional(Type.Boolean()), - local: Type.Object({ - path: Type.String() - }) + })) }, { }) @@ -29,11 +22,6 @@ const PlatformaticSkipSchema = Type.Object({ }, { }) -const PlatformaticServiceSchema = Type.Union([ - PlatformaticServiceWithImageSchema, - PlatformaticServiceWithLocalSchema -]) - const K3dRegistry = Type.Object({ address: Type.String(), configPath: Type.String(), From 6c7806523eccc70bed808db219b8a1b1ad5e91c0 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Wed, 1 Oct 2025 17:31:53 +0200 Subject: [PATCH 02/14] development profile support Signed-off-by: marcopiraccini --- cli/cluster.js | 2 +- lib/context.js | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/cli/cluster.js b/cli/cluster.js index d44479e..e785ac4 100644 --- a/cli/cluster.js +++ b/cli/cluster.js @@ -20,7 +20,7 @@ export default async function cli (argv) { }) const [cmd] = args._ - const context = await loadContext(args.profile) + const context = await loadContext(args.profile, { command: cmd }) debug.extend('cluster')(context.cluster) if (cmd === 'up') { diff --git a/lib/context.js b/lib/context.js index 48c943b..6878582 100644 --- a/lib/context.js +++ b/lib/context.js @@ -11,7 +11,7 @@ const deepmerge = Deepmerge() const userSecrets = { ...process.env } dotenv.config({ quiet: true, processEnv: userSecrets }) -export async function loadContext (profileNameOrPath) { +export async function loadContext (profileNameOrPath, options = {}) { let profilePath let profileName @@ -54,6 +54,30 @@ export async function loadContext (profileNameOrPath) { debug({ context }) + // Validate hot reload configuration for development profile (only when starting up) + if (options.command === 'up' && profileName === 'development' && context.platformatic.services) { + const requiredVars = [] + if (context.platformatic.services.icc?.hotReload) { + if (!userSecrets.ICC_REPO) { + requiredVars.push('ICC_REPO') + } + } + if (context.platformatic.services.machinist?.hotReload) { + if (!userSecrets.MACHINIST_REPO) { + requiredVars.push('MACHINIST_REPO') + } + } + + if (requiredVars.length > 0) { + throw new Error( + `Development profile, but the following environment variable(s) are not set: ${requiredVars.join(', ')}\n` + + 'Please set them before running the cluster:\n' + + requiredVars.map(v => ` export ${v}=/path/to/${v.toLowerCase().replace('_repo', '')}`).join('\n') + '\n' + + ` desk cluster up --profile ${profileName}` + ) + } + } + return context } From 7c2d4922567362cd41670ee2a4d020cf591f57fb Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Wed, 1 Oct 2025 17:45:25 +0200 Subject: [PATCH 03/14] uses correct helm Signed-off-by: marcopiraccini --- cli/cluster.js | 2 - lib/platformatic.js | 107 ++++++++++++++------------------------------ 2 files changed, 34 insertions(+), 75 deletions(-) diff --git a/cli/cluster.js b/cli/cluster.js index e785ac4..ce6bae4 100644 --- a/cli/cluster.js +++ b/cli/cluster.js @@ -47,8 +47,6 @@ export default async function cli (argv) { const infra = await platformatic.createChartConfig(context.platformatic, { context }) await installInfra(infra, { context }) - await platformatic.patchForDevMode(context.platformatic, { context }) - info('Waiting for Platformatic to finish starting') const k8sContext = { namespace: infra[platformatic.CHART_NAME].namespace diff --git a/lib/platformatic.js b/lib/platformatic.js index 7a135c0..5321e1c 100644 --- a/lib/platformatic.js +++ b/lib/platformatic.js @@ -37,6 +37,23 @@ export async function createChartConfig (values, { context }) { pullPolicy: 'IfNotPresent' } overrides.services.icc.log_level = 'debug' + overrides.services.icc.volumes = [{ + name: 'icc-local-repo', + hostPath: { + path: '/data/local/icc', + type: 'Directory' + } + }] + overrides.services.icc.volumeMounts = [{ + name: 'icc-local-repo', + mountPath: '/app' + }] + overrides.services.icc.command = ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] + overrides.services.icc.workingDir = '/app' + overrides.services.icc.env = [{ + name: 'DEV_K8S', + value: 'true' + }] } if (apps.machinist.hotReload && apps.machinist.localRepo) { @@ -46,82 +63,26 @@ export async function createChartConfig (values, { context }) { pullPolicy: 'IfNotPresent' } overrides.services.machinist.log_level = 'debug' + overrides.services.machinist.volumes = [{ + name: 'machinist-local-repo', + hostPath: { + path: '/data/local/machinist', + type: 'Directory' + } + }] + overrides.services.machinist.volumeMounts = [{ + name: 'machinist-local-repo', + mountPath: '/app' + }] + overrides.services.machinist.command = ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] + overrides.services.machinist.workingDir = '/app' + overrides.services.machinist.env = [{ + name: 'DEV_K8S', + value: 'true' + }] } const chartConfig = { ...values.chart, overrides } return { [CHART_NAME]: chartConfig } } - -export async function patchForDevMode (values, { context }) { - const { apps } = values - const namespace = 'platformatic' - - if (apps.icc.hotReload && apps.icc.localRepo) { - await patchDeployment('icc', { - volumes: [{ - name: 'icc-local-repo', - hostPath: { - path: '/data/local/icc', - type: 'Directory' - } - }], - volumeMounts: [{ - name: 'icc-local-repo', - mountPath: '/app' - }], - command: ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] - }, namespace, context) - } - - if (apps.machinist.hotReload && apps.machinist.localRepo) { - await patchDeployment('machinist', { - volumes: [{ - name: 'machinist-local-repo', - hostPath: { - path: '/data/local/machinist', - type: 'Directory' - } - }], - volumeMounts: [{ - name: 'machinist-local-repo', - mountPath: '/app' - }], - command: ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] - }, namespace, context) - } -} - -async function patchDeployment (name, config, namespace, context) { - const patch = { - spec: { - template: { - spec: { - volumes: config.volumes, - containers: [{ - name, - volumeMounts: config.volumeMounts, - command: config.command, - workingDir: '/app', - env: [ - { - name: 'DEV_K8S', - value: 'true' - } - ] - }] - } - } - } - } - - await spawn('kubectl', [ - '--context', context.kube.contextName, - '--namespace', namespace, - 'patch', - 'deployment', - name, - '--type', 'strategic', - '--patch', JSON.stringify(patch) - ]) -} From 20be0ab66dca1ad0c8ae81d9812f8ac801945906 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Wed, 1 Oct 2025 17:53:08 +0200 Subject: [PATCH 04/14] host path mount for development profile support Signed-off-by: marcopiraccini --- docker/icc/Dockerfile.dev | 21 +++++++++++++++++++++ docker/machinist/Dockerfile.dev | 21 +++++++++++++++++++++ lib/cluster/k3d.js | 29 +++++++++++++++++++++++------ lib/platformatic.js | 12 ++++-------- 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 docker/icc/Dockerfile.dev create mode 100644 docker/machinist/Dockerfile.dev diff --git a/docker/icc/Dockerfile.dev b/docker/icc/Dockerfile.dev new file mode 100644 index 0000000..8b77be6 --- /dev/null +++ b/docker/icc/Dockerfile.dev @@ -0,0 +1,21 @@ +FROM node:22.20.0-alpine + +ENV PNPM_HOME=/home/pnpm +ENV PATH=$PNPM_HOME:$PATH + +RUN mkdir $PNPM_HOME +RUN npm i pnpm@10 --location=global + +RUN apk add --update python3 make g++ gcompat && rm -rf /var/cache/apk/* + +WORKDIR /app + +# This CMD does the following at runtime (not build time) because: +# 1. /app is mounted as a volume from the host, so anything we COPY/RUN here during +# build would be overwritten by the volume mount +# 2. We need to use the exact code from the local repository +# 3. node_modules must be rebuilt in the container because native modules (like sodium-native) +# are compiled for the host OS and won't work in Alpine Linux +# 4. .env files from the host might have localhost connection strings that would override +# the Kubernetes environment variables +CMD sh -c 'find . -name ".env" -type f -delete && rm -rf node_modules && pnpm install && pnpm run dev' diff --git a/docker/machinist/Dockerfile.dev b/docker/machinist/Dockerfile.dev new file mode 100644 index 0000000..8b77be6 --- /dev/null +++ b/docker/machinist/Dockerfile.dev @@ -0,0 +1,21 @@ +FROM node:22.20.0-alpine + +ENV PNPM_HOME=/home/pnpm +ENV PATH=$PNPM_HOME:$PATH + +RUN mkdir $PNPM_HOME +RUN npm i pnpm@10 --location=global + +RUN apk add --update python3 make g++ gcompat && rm -rf /var/cache/apk/* + +WORKDIR /app + +# This CMD does the following at runtime (not build time) because: +# 1. /app is mounted as a volume from the host, so anything we COPY/RUN here during +# build would be overwritten by the volume mount +# 2. We need to use the exact code from the local repository +# 3. node_modules must be rebuilt in the container because native modules (like sodium-native) +# are compiled for the host OS and won't work in Alpine Linux +# 4. .env files from the host might have localhost connection strings that would override +# the Kubernetes environment variables +CMD sh -c 'find . -name ".env" -type f -delete && rm -rf node_modules && pnpm install && pnpm run dev' diff --git a/lib/cluster/k3d.js b/lib/cluster/k3d.js index 6722867..65fa4e0 100644 --- a/lib/cluster/k3d.js +++ b/lib/cluster/k3d.js @@ -1,7 +1,10 @@ -import { join } from 'node:path' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' import { spawn, clusterName } from '../utils.js' import * as kubectl from '../kubectl.js' +const __dirname = dirname(fileURLToPath(import.meta.url)) + export async function startCluster ({ provider, chartDir, name, platformatic }) { const { config } = provider @@ -42,11 +45,25 @@ export async function startCluster ({ provider, chartDir, name, platformatic }) await spawn('k3d', args) if (needsNodeImage) { - try { - await spawn('docker', ['pull', 'node:22.20.0-alpine']) - await spawn('k3d', ['image', 'import', 'node:22.20.0-alpine', '-c', clusterName(name)]) - } catch (err) { - // Image might already be imported, continue + const dockerDir = join(__dirname, '..', '..', 'docker') + + // Build and import dev images for services with hot reload + if (platformatic.services?.icc?.hotReload) { + try { + await spawn('docker', ['build', '-t', 'platformatic/icc:dev', '-f', join(dockerDir, 'icc', 'Dockerfile.dev'), dockerDir]) + await spawn('k3d', ['image', 'import', 'platformatic/icc:dev', '-c', clusterName(name)]) + } catch (err) { + // Image might already exist, continue + } + } + + if (platformatic.services?.machinist?.hotReload) { + try { + await spawn('docker', ['build', '-t', 'platformatic/machinist:dev', '-f', join(dockerDir, 'machinist', 'Dockerfile.dev'), dockerDir]) + await spawn('k3d', ['image', 'import', 'platformatic/machinist:dev', '-c', clusterName(name)]) + } catch (err) { + // Image might already exist, continue + } } } } diff --git a/lib/platformatic.js b/lib/platformatic.js index 5321e1c..cffc1f1 100644 --- a/lib/platformatic.js +++ b/lib/platformatic.js @@ -32,8 +32,8 @@ export async function createChartConfig (values, { context }) { if (apps.icc.hotReload && apps.icc.localRepo) { overrides.services.icc.image = { - repository: 'node', - tag: '22.20.0-alpine', + repository: 'platformatic/icc', + tag: 'dev', pullPolicy: 'IfNotPresent' } overrides.services.icc.log_level = 'debug' @@ -48,8 +48,6 @@ export async function createChartConfig (values, { context }) { name: 'icc-local-repo', mountPath: '/app' }] - overrides.services.icc.command = ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] - overrides.services.icc.workingDir = '/app' overrides.services.icc.env = [{ name: 'DEV_K8S', value: 'true' @@ -58,8 +56,8 @@ export async function createChartConfig (values, { context }) { if (apps.machinist.hotReload && apps.machinist.localRepo) { overrides.services.machinist.image = { - repository: 'node', - tag: '22.20.0-alpine', + repository: 'platformatic/machinist', + tag: 'dev', pullPolicy: 'IfNotPresent' } overrides.services.machinist.log_level = 'debug' @@ -74,8 +72,6 @@ export async function createChartConfig (values, { context }) { name: 'machinist-local-repo', mountPath: '/app' }] - overrides.services.machinist.command = ['sh', '-c', 'apk add --no-cache python3 make g++ gcompat && npm install -g pnpm@10 && rm -rf node_modules && find . -name ".env" -type f -delete && pnpm install && pnpm run dev'] - overrides.services.machinist.workingDir = '/app' overrides.services.machinist.env = [{ name: 'DEV_K8S', value: 'true' From dd0912ef31ddf04f0f733a74bb1673f2fed12cf9 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Thu, 2 Oct 2025 06:02:42 +0200 Subject: [PATCH 05/14] Moved log level to development profile Signed-off-by: marcopiraccini --- lib/platformatic.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/platformatic.js b/lib/platformatic.js index cffc1f1..dfd5764 100644 --- a/lib/platformatic.js +++ b/lib/platformatic.js @@ -36,7 +36,6 @@ export async function createChartConfig (values, { context }) { tag: 'dev', pullPolicy: 'IfNotPresent' } - overrides.services.icc.log_level = 'debug' overrides.services.icc.volumes = [{ name: 'icc-local-repo', hostPath: { @@ -60,7 +59,6 @@ export async function createChartConfig (values, { context }) { tag: 'dev', pullPolicy: 'IfNotPresent' } - overrides.services.machinist.log_level = 'debug' overrides.services.machinist.volumes = [{ name: 'machinist-local-repo', hostPath: { From 396ea20d0b1342c739fed94c405bac152c438b64 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Thu, 2 Oct 2025 06:04:03 +0200 Subject: [PATCH 06/14] Remove useless default Signed-off-by: marcopiraccini --- lib/platformatic.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/platformatic.js b/lib/platformatic.js index dfd5764..6b71bbb 100644 --- a/lib/platformatic.js +++ b/lib/platformatic.js @@ -18,10 +18,10 @@ export async function createChartConfig (values, { context }) { VALKEY_ICC_CONNECTION_STRING: clusterStatus.valkey.connectionString, PROMETHEUS_URL: clusterStatus.prometheus.url, PUBLIC_URL: 'https://icc.plt', - ICC_IMAGE_REPO: apps.icc.image?.repository || 'platformatic/intelligent-command-center', - ICC_IMAGE_TAG: apps.icc.image?.tag || 'latest', - MACHINIST_IMAGE_REPO: apps.machinist.image?.repository || 'platformatic/machinist', - MACHINIST_IMAGE_TAG: apps.machinist.image?.tag || 'latest', + ICC_IMAGE_REPO: apps.icc.image?.repository, + ICC_IMAGE_TAG: apps.icc.image?.tag, + MACHINIST_IMAGE_REPO: apps.machinist.image?.repository, + MACHINIST_IMAGE_TAG: apps.machinist.image?.tag, PLT_NAMESPACES: context.cluster.namespaces } const overrideValues = await loadYamlFile(join(context.chartDir, CHART_NAME, 'overrides.yaml'), substitutions) From 2b66fbf7dfb77cf92a62bb17ca01c7d67661ae9d Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Thu, 2 Oct 2025 06:21:54 +0200 Subject: [PATCH 07/14] after review Signed-off-by: marcopiraccini --- docker/icc/Dockerfile.dev | 4 ++-- docker/machinist/Dockerfile.dev | 4 ++-- lib/context.js | 3 ++- profiles/development.yaml | 5 ----- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docker/icc/Dockerfile.dev b/docker/icc/Dockerfile.dev index 8b77be6..522e336 100644 --- a/docker/icc/Dockerfile.dev +++ b/docker/icc/Dockerfile.dev @@ -17,5 +17,5 @@ WORKDIR /app # 3. node_modules must be rebuilt in the container because native modules (like sodium-native) # are compiled for the host OS and won't work in Alpine Linux # 4. .env files from the host might have localhost connection strings that would override -# the Kubernetes environment variables -CMD sh -c 'find . -name ".env" -type f -delete && rm -rf node_modules && pnpm install && pnpm run dev' +# the Kubernetes environment variables, so we rename them temporarily +CMD sh -c 'find . -name ".env" -type f -exec mv {} {}.backup-env \; && rm -rf node_modules && pnpm install && pnpm run dev' diff --git a/docker/machinist/Dockerfile.dev b/docker/machinist/Dockerfile.dev index 8b77be6..522e336 100644 --- a/docker/machinist/Dockerfile.dev +++ b/docker/machinist/Dockerfile.dev @@ -17,5 +17,5 @@ WORKDIR /app # 3. node_modules must be rebuilt in the container because native modules (like sodium-native) # are compiled for the host OS and won't work in Alpine Linux # 4. .env files from the host might have localhost connection strings that would override -# the Kubernetes environment variables -CMD sh -c 'find . -name ".env" -type f -delete && rm -rf node_modules && pnpm install && pnpm run dev' +# the Kubernetes environment variables, so we rename them temporarily +CMD sh -c 'find . -name ".env" -type f -exec mv {} {}.backup-env \; && rm -rf node_modules && pnpm install && pnpm run dev' diff --git a/lib/context.js b/lib/context.js index 6878582..55153a1 100644 --- a/lib/context.js +++ b/lib/context.js @@ -54,7 +54,8 @@ export async function loadContext (profileNameOrPath, options = {}) { debug({ context }) - // Validate hot reload configuration for development profile (only when starting up) + // Validate hot reload configuration for development profile + // Only validate when running 'cluster up' command, not 'cluster down' or 'cluster status' if (options.command === 'up' && profileName === 'development' && context.platformatic.services) { const requiredVars = [] if (context.platformatic.services.icc?.hotReload) { diff --git a/profiles/development.yaml b/profiles/development.yaml index 20f899e..1f55f9e 100644 --- a/profiles/development.yaml +++ b/profiles/development.yaml @@ -24,11 +24,6 @@ dependencies: plt_defaults: false platformatic: - imagePullSecret: - registry: docker.io - user: "{{ PULL_SECRET_USER }}" - token: "{{ PULL_SECRET_TOKEN }}" - services: icc: hotReload: true From 1016124068ef1fd6c231115af401d59a3d21e891 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Thu, 2 Oct 2025 10:42:32 +0200 Subject: [PATCH 08/14] more changes Signed-off-by: marcopiraccini --- charts/v4/config.yaml | 2 +- lib/helm.js | 1 + lib/platformatic.js | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/charts/v4/config.yaml b/charts/v4/config.yaml index de9516e..e4a0991 100644 --- a/charts/v4/config.yaml +++ b/charts/v4/config.yaml @@ -27,7 +27,7 @@ dependencies: platformatic/helm: location: 'oci://ghcr.io/platformatic/helm' releaseName: 'platformatic' - version: '4.0.0' + version: '4.0.2-alpha3' namespace: 'platformatic' prometheus-community/prometheus-adapter: diff --git a/lib/helm.js b/lib/helm.js index 55cc459..380e4e0 100644 --- a/lib/helm.js +++ b/lib/helm.js @@ -43,6 +43,7 @@ export async function apply (releaseName, chart, valueFilePaths = [], version = return parseNotes(stdout) } catch (err) { debug({ err }) + warn(`Helm error output: ${err.output}`) const errorDetail = parseHelmError(err.output) warn(`Applying Helm for release '${errorDetail.releaseName}' failed. Waiting for '${errorDetail.apiVersion}/${errorDetail.kind}' to be ready`) const crdName = await getCrdName(errorDetail.kind, errorDetail.apiVersion) diff --git a/lib/platformatic.js b/lib/platformatic.js index 6b71bbb..ef82331 100644 --- a/lib/platformatic.js +++ b/lib/platformatic.js @@ -18,12 +18,22 @@ export async function createChartConfig (values, { context }) { VALKEY_ICC_CONNECTION_STRING: clusterStatus.valkey.connectionString, PROMETHEUS_URL: clusterStatus.prometheus.url, PUBLIC_URL: 'https://icc.plt', - ICC_IMAGE_REPO: apps.icc.image?.repository, - ICC_IMAGE_TAG: apps.icc.image?.tag, - MACHINIST_IMAGE_REPO: apps.machinist.image?.repository, - MACHINIST_IMAGE_TAG: apps.machinist.image?.tag, PLT_NAMESPACES: context.cluster.namespaces } + + // Only add image values if they're defined + if (apps.icc.image?.repository) { + substitutions.ICC_IMAGE_REPO = apps.icc.image.repository + } + if (apps.icc.image?.tag) { + substitutions.ICC_IMAGE_TAG = apps.icc.image.tag + } + if (apps.machinist.image?.repository) { + substitutions.MACHINIST_IMAGE_REPO = apps.machinist.image.repository + } + if (apps.machinist.image?.tag) { + substitutions.MACHINIST_IMAGE_TAG = apps.machinist.image.tag + } const overrideValues = await loadYamlFile(join(context.chartDir, CHART_NAME, 'overrides.yaml'), substitutions) const deskValues = { ...values } delete deskValues.chart From cfd8e2473fa71ac7dab36a172aab10536232d52a Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Thu, 2 Oct 2025 11:26:15 +0200 Subject: [PATCH 09/14] use only one Dockerfile Signed-off-by: marcopiraccini --- docker/{icc => }/Dockerfile.dev | 0 docker/machinist/Dockerfile.dev | 21 --------------------- lib/cluster/k3d.js | 5 +++-- 3 files changed, 3 insertions(+), 23 deletions(-) rename docker/{icc => }/Dockerfile.dev (100%) delete mode 100644 docker/machinist/Dockerfile.dev diff --git a/docker/icc/Dockerfile.dev b/docker/Dockerfile.dev similarity index 100% rename from docker/icc/Dockerfile.dev rename to docker/Dockerfile.dev diff --git a/docker/machinist/Dockerfile.dev b/docker/machinist/Dockerfile.dev deleted file mode 100644 index 522e336..0000000 --- a/docker/machinist/Dockerfile.dev +++ /dev/null @@ -1,21 +0,0 @@ -FROM node:22.20.0-alpine - -ENV PNPM_HOME=/home/pnpm -ENV PATH=$PNPM_HOME:$PATH - -RUN mkdir $PNPM_HOME -RUN npm i pnpm@10 --location=global - -RUN apk add --update python3 make g++ gcompat && rm -rf /var/cache/apk/* - -WORKDIR /app - -# This CMD does the following at runtime (not build time) because: -# 1. /app is mounted as a volume from the host, so anything we COPY/RUN here during -# build would be overwritten by the volume mount -# 2. We need to use the exact code from the local repository -# 3. node_modules must be rebuilt in the container because native modules (like sodium-native) -# are compiled for the host OS and won't work in Alpine Linux -# 4. .env files from the host might have localhost connection strings that would override -# the Kubernetes environment variables, so we rename them temporarily -CMD sh -c 'find . -name ".env" -type f -exec mv {} {}.backup-env \; && rm -rf node_modules && pnpm install && pnpm run dev' diff --git a/lib/cluster/k3d.js b/lib/cluster/k3d.js index 65fa4e0..5bcc655 100644 --- a/lib/cluster/k3d.js +++ b/lib/cluster/k3d.js @@ -46,11 +46,12 @@ export async function startCluster ({ provider, chartDir, name, platformatic }) if (needsNodeImage) { const dockerDir = join(__dirname, '..', '..', 'docker') + const devDockerfile = join(dockerDir, 'Dockerfile.dev') // Build and import dev images for services with hot reload if (platformatic.services?.icc?.hotReload) { try { - await spawn('docker', ['build', '-t', 'platformatic/icc:dev', '-f', join(dockerDir, 'icc', 'Dockerfile.dev'), dockerDir]) + await spawn('docker', ['build', '-t', 'platformatic/icc:dev', '-f', devDockerfile, dockerDir]) await spawn('k3d', ['image', 'import', 'platformatic/icc:dev', '-c', clusterName(name)]) } catch (err) { // Image might already exist, continue @@ -59,7 +60,7 @@ export async function startCluster ({ provider, chartDir, name, platformatic }) if (platformatic.services?.machinist?.hotReload) { try { - await spawn('docker', ['build', '-t', 'platformatic/machinist:dev', '-f', join(dockerDir, 'machinist', 'Dockerfile.dev'), dockerDir]) + await spawn('docker', ['build', '-t', 'platformatic/machinist:dev', '-f', devDockerfile, dockerDir]) await spawn('k3d', ['image', 'import', 'platformatic/machinist:dev', '-c', clusterName(name)]) } catch (err) { // Image might already exist, continue From eb557655db035e648bfe2c00737bfe0c1838c95d Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Thu, 2 Oct 2025 11:33:54 +0200 Subject: [PATCH 10/14] use only one Dockerfile Signed-off-by: marcopiraccini --- .env.sample | 6 +++++- README.md | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.env.sample b/.env.sample index 180d116..5c7a3b8 100644 --- a/.env.sample +++ b/.env.sample @@ -3,6 +3,10 @@ GITHUB_OAUTH_CLIENT_ID= GITHUB_OAUTH_CLIENT_SECRET= GITHUB_OAUTH_VALID_EMAILS= -# Use a Docker PAT, Docker OAT, or Github PAT IF NECESSARY +# Use a Docker PAT, Docker OAT, or Github PAT IF NECESSARY (for private repos) # PULL_SECRET_USER= # PULL_SECRET_TOKEN= + +# Development profile: path to local ICC/machinist repository +# ICC_REPO=/work/workspaces/workspace-platformatic/icc3 +# MACHINIST_REPO=/work/workspaces/workspace-platformatic/machinist diff --git a/README.md b/README.md index 4a93906..8b49a6a 100644 --- a/README.md +++ b/README.md @@ -146,11 +146,11 @@ to: ### Development Profile -The `development` profile enables hot reloading for ICC and Machinist services using local repositories: +The `development` profile enables hot reloading for ICC and Machinist services using local repositories. + +First uncomment/set the `ICC_REPO` and `MACHINIST_REPO` variables on `.env`, then: ```sh -export ICC_REPO=/path/to/icc3 -export MACHINIST_REPO=/path/to/machinist desk cluster up --profile development ``` From 31ff9caaaa226192d9abff09448c037597090e08 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Fri, 3 Oct 2025 11:42:35 +0200 Subject: [PATCH 11/14] removed images form development profile Signed-off-by: marcopiraccini --- profiles/development.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/profiles/development.yaml b/profiles/development.yaml index 1f55f9e..e298e38 100644 --- a/profiles/development.yaml +++ b/profiles/development.yaml @@ -29,10 +29,6 @@ platformatic: hotReload: true localRepo: "{{ ICC_REPO }}" - image: - tag: "dev" - repository: "platformatic/intelligent-command-center" - features: cache_recommendations: enable: false @@ -66,10 +62,6 @@ platformatic: hotReload: true localRepo: "{{ MACHINIST_REPO }}" - image: - tag: "dev" - repository: "platformatic/machinist" - features: event_export: enable: false From 8c930ed992569641fbc49b8eb23c7df1a82ee7c9 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Fri, 3 Oct 2025 16:21:03 +0200 Subject: [PATCH 12/14] use current user in container for pnpm install Signed-off-by: marcopiraccini --- docker/Dockerfile.dev | 7 +++++++ lib/platformatic.js | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 522e336..4190e17 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -8,7 +8,14 @@ RUN npm i pnpm@10 --location=global RUN apk add --update python3 make g++ gcompat && rm -rf /var/cache/apk/* +# Create a non-root user to avoid creating root-owned files in mounted volumes +RUN addgroup -g 1000 appuser && adduser -u 1000 -G appuser -s /bin/sh -D appuser +RUN chown -R appuser:appuser $PNPM_HOME + WORKDIR /app +RUN chown appuser:appuser /app + +USER appuser # This CMD does the following at runtime (not build time) because: # 1. /app is mounted as a volume from the host, so anything we COPY/RUN here during diff --git a/lib/platformatic.js b/lib/platformatic.js index ef82331..a728083 100644 --- a/lib/platformatic.js +++ b/lib/platformatic.js @@ -46,6 +46,15 @@ export async function createChartConfig (values, { context }) { tag: 'dev', pullPolicy: 'IfNotPresent' } + overrides.services.icc.podSecurityContext = { + runAsUser: 1000, + runAsGroup: 1000, + fsGroup: 1000 + } + overrides.services.icc.securityContext = { + runAsNonRoot: true, + runAsUser: 1000 + } overrides.services.icc.volumes = [{ name: 'icc-local-repo', hostPath: { @@ -69,6 +78,15 @@ export async function createChartConfig (values, { context }) { tag: 'dev', pullPolicy: 'IfNotPresent' } + overrides.services.machinist.podSecurityContext = { + runAsUser: 1000, + runAsGroup: 1000, + fsGroup: 1000 + } + overrides.services.machinist.securityContext = { + runAsNonRoot: true, + runAsUser: 1000 + } overrides.services.machinist.volumes = [{ name: 'machinist-local-repo', hostPath: { From 622786365db845e973f5b3b7d6d4c43d30fea288 Mon Sep 17 00:00:00 2001 From: Leonardo Rossi Date: Mon, 6 Oct 2025 15:46:12 +0200 Subject: [PATCH 13/14] fixes start container timeout Signed-off-by: Leonardo Rossi --- docker/Dockerfile.dev | 19 +------------------ lib/cluster/k3d.js | 9 ++------- lib/context.js | 11 ++++++++++- lib/utils.js | 4 ++-- 4 files changed, 15 insertions(+), 28 deletions(-) diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 4190e17..82c26e8 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -6,23 +6,6 @@ ENV PATH=$PNPM_HOME:$PATH RUN mkdir $PNPM_HOME RUN npm i pnpm@10 --location=global -RUN apk add --update python3 make g++ gcompat && rm -rf /var/cache/apk/* - -# Create a non-root user to avoid creating root-owned files in mounted volumes -RUN addgroup -g 1000 appuser && adduser -u 1000 -G appuser -s /bin/sh -D appuser -RUN chown -R appuser:appuser $PNPM_HOME - WORKDIR /app -RUN chown appuser:appuser /app - -USER appuser -# This CMD does the following at runtime (not build time) because: -# 1. /app is mounted as a volume from the host, so anything we COPY/RUN here during -# build would be overwritten by the volume mount -# 2. We need to use the exact code from the local repository -# 3. node_modules must be rebuilt in the container because native modules (like sodium-native) -# are compiled for the host OS and won't work in Alpine Linux -# 4. .env files from the host might have localhost connection strings that would override -# the Kubernetes environment variables, so we rename them temporarily -CMD sh -c 'find . -name ".env" -type f -exec mv {} {}.backup-env \; && rm -rf node_modules && pnpm install && pnpm run dev' +CMD sh -c 'find . -name ".env" -type f -exec mv {} {}.backup-env \;' && pnpm run dev diff --git a/lib/cluster/k3d.js b/lib/cluster/k3d.js index 5bcc655..f3db925 100644 --- a/lib/cluster/k3d.js +++ b/lib/cluster/k3d.js @@ -39,7 +39,6 @@ export async function startCluster ({ provider, chartDir, name, platformatic }) needsNodeImage = true }) } - args = args.concat(volumes) await spawn('k3d', args) @@ -50,12 +49,8 @@ export async function startCluster ({ provider, chartDir, name, platformatic }) // Build and import dev images for services with hot reload if (platformatic.services?.icc?.hotReload) { - try { - await spawn('docker', ['build', '-t', 'platformatic/icc:dev', '-f', devDockerfile, dockerDir]) - await spawn('k3d', ['image', 'import', 'platformatic/icc:dev', '-c', clusterName(name)]) - } catch (err) { - // Image might already exist, continue - } + await spawn('docker', ['build', '-t', 'platformatic/icc:dev', '-f', devDockerfile, dockerDir]) + await spawn('k3d', ['image', 'import', 'platformatic/icc:dev', '-c', clusterName(name)]) } if (platformatic.services?.machinist?.hotReload) { diff --git a/lib/context.js b/lib/context.js index 55153a1..d219d66 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1,7 +1,7 @@ import { join, resolve, isAbsolute } from 'node:path' import Deepmerge from '@fastify/deepmerge' import dotenv from 'dotenv' -import { clusterName, debug, loadYamlFile, warn } from './utils.js' +import { clusterName, debug, loadYamlFile, spawn, warn } from './utils.js' import { createRunDir } from './run-directory.js' import { parseProfile } from '../schemas/profile.js' import { parseConfig } from '../schemas/config.js' @@ -77,6 +77,15 @@ export async function loadContext (profileNameOrPath, options = {}) { ` desk cluster up --profile ${profileName}` ) } + + // run if arch is not intel + debug({ arch: process.arch }) + if (process.arch !== 'x64') { + const iccRepo = userSecrets.ICC_REPO + const pnpmInstallCommand = `NPM_CONFIG_PLATFORM=linux NPM_CONFIG_ARCH=arm64 pnpm install --force` + // run it in iccRepo directory + await spawn('bash', ['-c', pnpmInstallCommand], { cwd: iccRepo }) + } } return context diff --git a/lib/utils.js b/lib/utils.js index 4a0b5a0..04df10a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -36,9 +36,9 @@ export function error (msg, ...args) { } } -export function spawn (cmd, args = []) { +export function spawn (cmd, args = [], options = {}) { debug(`Executing CLI: ${cmd} ${args.join(' ')}`) - return nanospawn(cmd, args) + return nanospawn(cmd, args, options) } export async function spawnWithOutput (cmd, args = []) { From bc4488bc054c5d6ad8e27173239d0e284daba139 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Mon, 6 Oct 2025 16:36:11 +0200 Subject: [PATCH 14/14] Docker base image on debian/slim fpr development Signed-off-by: marcopiraccini --- docker/Dockerfile.dev | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 82c26e8..5f07e1b 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,4 +1,7 @@ -FROM node:22.20.0-alpine +# Using Debian-based slim image instead of Alpine because native addons +# (like sodium-native used by @fastify/secure-session) have prebuilt binaries +# for glibc (Debian) but not musl (Alpine), avoiding rebuild issues +FROM node:22.20.0-slim ENV PNPM_HOME=/home/pnpm ENV PATH=$PNPM_HOME:$PATH