Skip to content
Merged
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
10 changes: 7 additions & 3 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
PULL_SECRET_USER=
PULL_SECRET_TOKEN=
# 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
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.

First uncomment/set the `ICC_REPO` and `MACHINIST_REPO` variables on `.env`, then:

```sh
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
2 changes: 1 addition & 1 deletion charts/v4/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 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 Down
14 changes: 14 additions & 0 deletions docker/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 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

RUN mkdir $PNPM_HOME
RUN npm i pnpm@10 --location=global

WORKDIR /app

CMD sh -c 'find . -name ".env" -type f -exec mv {} {}.backup-env \;' && pnpm run dev
39 changes: 34 additions & 5 deletions lib/cluster/k3d.js
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -26,13 +29,39 @@ 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) {
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) {
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) {
try {
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
}
}
}
Comment thread
MzUgM marked this conversation as resolved.
}

export async function stopCluster ({ name }) {
Expand Down
40 changes: 37 additions & 3 deletions lib/context.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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'
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,40 @@ export async function loadContext (profileNameOrPath) {

debug({ context })

// 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) {
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}`
)
}

// 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
}

Expand Down
1 change: 1 addition & 0 deletions lib/helm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
85 changes: 80 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,17 +18,92 @@ 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

const overrides = deepmerge(overrideValues, deskValues)

if (apps.icc.hotReload && apps.icc.localRepo) {
overrides.services.icc.image = {
repository: 'platformatic/icc',
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: {
path: '/data/local/icc',
type: 'Directory'
}
}]
overrides.services.icc.volumeMounts = [{
name: 'icc-local-repo',
mountPath: '/app'
}]
overrides.services.icc.env = [{
name: 'DEV_K8S',
value: 'true'
}]
}

if (apps.machinist.hotReload && apps.machinist.localRepo) {
overrides.services.machinist.image = {
repository: 'platformatic/machinist',
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: {
path: '/data/local/machinist',
type: 'Directory'
}
}]
overrides.services.machinist.volumeMounts = [{
name: 'machinist-local-repo',
mountPath: '/app'
}]
overrides.services.machinist.env = [{
name: 'DEV_K8S',
value: 'true'
}]
}

const chartConfig = { ...values.chart, overrides }

return { [CHART_NAME]: chartConfig }
Expand Down
4 changes: 2 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []) {
Expand Down
69 changes: 69 additions & 0 deletions profiles/development.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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:
services:
icc:
hotReload: true
localRepo: "{{ ICC_REPO }}"

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="
Comment thread
MzUgM marked this conversation as resolved.
control_plane_keys: "iUIz122f2Kh49Q2PvGxJWuajJRm8B0TZ7orfGbf29LA="
user_manager_session: "XnlPIbATw2x/7xIX304esO9qKuCZLR3HOa8wTF6O3pc="

machinist:
hotReload: true
localRepo: "{{ MACHINIST_REPO }}"

features:
event_export:
enable: false

log_level: debug
Loading