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..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') { @@ -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..6878582 100644 --- a/lib/context.js +++ b/lib/context.js @@ -8,10 +8,10 @@ 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) { +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 } 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(),