Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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=
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion cli/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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
Expand Down
22 changes: 19 additions & 3 deletions lib/cluster/k3d.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down
28 changes: 26 additions & 2 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
}

Expand Down
102 changes: 97 additions & 5 deletions lib/platformatic.js
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -18,18 +18,110 @@ 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)
const deskValues = { ...values }
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)
])
}
82 changes: 82 additions & 0 deletions profiles/development.yaml
Original file line number Diff line number Diff line change
@@ -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
24 changes: 6 additions & 18 deletions schemas/v4/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
}))
}, {
})

Expand All @@ -29,11 +22,6 @@ const PlatformaticSkipSchema = Type.Object({
}, {
})

const PlatformaticServiceSchema = Type.Union([
PlatformaticServiceWithImageSchema,
PlatformaticServiceWithLocalSchema
])

const K3dRegistry = Type.Object({
address: Type.String(),
configPath: Type.String(),
Expand Down