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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,40 @@ Deploy with an environment file:
desk deploy --profile <name> --dir ./my-watt-project --envfile ./my-watt-project/.env
```

#### Deploy through ICC's deploy API (`--via-icc`)

By default `desk` templates the Deployment + Service and applies them directly
(the skew-protection `observe` model: you create the workload, ICC manages
routing). Use `--via-icc` to instead drive the deploy through ICC's deploy API,
which lets you exercise the `manage` and `advise` actuation modes:

```sh
desk deploy --profile skew-protection --via-icc \
--app-id <ICC application id> --deploy-token plt_deploy_... \
--image <pre-existing image> --version v1 --min-replicas 1
```

What happens depends on the app's mode (Settings → Skew Protection → Mode):

* `manage` — ICC creates the Deployment + Service itself; `desk` applies nothing.
* `advise` — ICC returns the manifests as a plan and `desk` applies them with
`kubectl` (use `--dry-run` to print the plan without applying).
* `observe` — ICC rejects the deploy API (this is the default direct path above).

Flags:

* `--app-id` — the ICC application UUID (from the app URL `…/watts/<id>`). Required.
* `--deploy-token` — a scoped deploy token (`plt_deploy_…`), or set
`PLT_DEPLOY_TOKEN`. Mint one in the app's Settings → Deploy Tokens. Required.
* `--icc-url` — ICC base URL (default `https://icc.plt`). TLS verification is
disabled for this call (local self-signed cert); for local testing only.

`--via-icc` still builds/pushes the image when `--dir` is used; the image must
exist before ICC can reference it (`--image <ref>` for a prebuilt one). `manage`
mode also requires the `plt-pod-manager` RBAC to create Deployments/Services
(shipped in the helm chart; run `helm upgrade`). See
`skew-protection/TESTING.md` for the full manual test walkthrough.

## Troubleshooting

Use `DEBUG=plt-desk*` to view debug statements. The output can be narrowed down
Expand Down
40 changes: 37 additions & 3 deletions cli/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { loadContext } from '../lib/context.js'
import { error, info } from '../lib/utils.js'
import * as registry from '../lib/registry.js'
import * as deploy from '../lib/deploy.js'
import { deployViaIcc, handleIccDeploy } from '../lib/icc.js'
import { getClusterStatus } from '../lib/cluster/index.js'

export const options = { command: 'deploy', strict: true }

export default async function cli (argv) {
const args = minimist(argv, {
bool: ['dry-run', 'headless'],
bool: ['dry-run', 'headless', 'via-icc'],
string: [
'dir',
'image',
Expand All @@ -24,7 +25,10 @@ export default async function cli (argv) {
'replicas',
'min-replicas',
'max-replicas',
'npmrc'
'npmrc',
'app-id',
'deploy-token',
'icc-url'
],
alias: {
dir: 'd',
Expand All @@ -36,7 +40,8 @@ export default async function cli (argv) {
hostname: 'h'
},
default: {
namespace: 'platformatic'
namespace: 'platformatic',
'icc-url': 'https://icc.plt'
}
})

Expand Down Expand Up @@ -117,6 +122,35 @@ export default async function cli (argv) {
}
}

// ICC-driven deploy: hand the image to ICC's deploy API and let the app's
// actuation mode decide (manage = ICC creates the workload; advise = ICC
// returns manifests that desk applies). This is the CI path a customer uses,
// and the way to exercise manage/advise modes end to end.
if (args['via-icc']) {
const appId = args['app-id']
const token = args['deploy-token'] || process.env.PLT_DEPLOY_TOKEN
if (!appId) { error('--via-icc requires --app-id <ICC application id>'); process.exit(1) }
if (!token) { error('--via-icc requires --deploy-token <plt_deploy_...> or PLT_DEPLOY_TOKEN'); process.exit(1) }
if (!version) { error('--via-icc requires --version <label>'); process.exit(1) }

info(`\nDeploying ${appName}:${version} through ICC (${args['icc-url']}) with image ${appImage}`)
const result = await deployViaIcc({
iccUrl: args['icc-url'],
appId,
token,
image: appImage,
version,
hostname,
namespace: args.namespace,
minReplicas,
maxReplicas,
env: Object.keys(envVars).length ? envVars : undefined
})
info(`ICC actuation mode: ${result.mode}`)
await handleIccDeploy(context, args.namespace, result, args['dry-run'])
return
}

await deploy.createDeployment(appName, appImage, args.namespace, envVars, args['dry-run'], { context, version, isWorkflow, hostname, minReplicas, maxReplicas })
await deploy.createService(appName, appImage, args.namespace, args['dry-run'], { context, version, isWorkflow, headless: args.headless })

Expand Down
75 changes: 75 additions & 0 deletions lib/icc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { addToRun } from './run-directory.js'
import { spawn, info, warn } from './utils.js'

// Drive a deploy through ICC's deploy API instead of templating + applying the
// workload directly. Lets you exercise the skew-protection actuation modes:
// manage -> ICC creates the Deployment + Service itself (nothing to apply here)
// advise -> ICC returns the manifests as a plan; desk applies them (below)
// observe -> ICC rejects the deploy API (you create workloads yourself)
//
// Auth is a scoped deploy token (Bearer plt_deploy_...), the same CI path a
// customer would use. icc.plt uses a local/self-signed cert, so TLS verification
// is disabled for this call (dev/testing tool).
export async function deployViaIcc ({ iccUrl, appId, token, image, version, hostname, namespace, minReplicas, maxReplicas, env }) {
const url = `${iccUrl.replace(/\/+$/, '')}/control-plane/applications/${appId}/deploy`

const body = { image, version }
if (hostname) body.hostname = hostname
if (namespace) body.namespace = namespace
if (minReplicas) body.minReplicas = minReplicas
if (maxReplicas) body.maxReplicas = maxReplicas
if (env && Object.keys(env).length) body.env = env

const prevTls = process.env.NODE_TLS_REJECT_UNAUTHORIZED
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
body: JSON.stringify(body)
})
const text = await res.text()
let json
try { json = JSON.parse(text) } catch { json = { raw: text } }
if (!res.ok) {
throw new Error(`ICC deploy failed (${res.status}): ${json?.message ?? text}`)
}
return json
} finally {
if (prevTls === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
else process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevTls
}
}

// Apply the manifests from an advise-mode plan via kubectl (the external actor's
// job). Each step's manifest is written to the run dir and applied.
export async function applyIccPlan (context, namespace, plan, dryRun) {
let applied = 0
for (const step of plan) {
if (!step.manifest) continue
const name = step.manifest?.metadata?.name ?? `${step.kind ?? 'resource'}-${applied}`
info(` plan: ${step.kind}/${step.action} ${name}`)
if (step.command) info(` ${step.command}`)
if (dryRun) { applied++; continue }
const filePath = await addToRun(context.runDir, `icc-${step.kind}-${name}.json`, JSON.stringify(step.manifest))
await spawn('kubectl', [`--namespace=${namespace}`, 'apply', `--filename=${filePath}`])
applied++
}
return applied
}

// Report the outcome of an ICC deploy and, in advise mode, apply the plan.
export async function handleIccDeploy (context, namespace, result, dryRun) {
if (result.deployed) {
info('\nManage mode: ICC created the Deployment + Service. Pods will register and the version will go active.')
return
}
const plan = result.plan ?? []
if (result.pendingApply || plan.length > 0) {
info(`\nAdvise mode: ICC returned a ${plan.length}-step plan. Applying it now (external actor):`)
const applied = await applyIccPlan(context, namespace, plan, dryRun)
info(`\nApplied ${applied} manifest(s). ICC confirms the version active once pods register and the gateway route is Accepted.`)
return
}
warn('ICC deploy returned no plan and deployed=false; nothing to do.')
}
1 change: 1 addition & 0 deletions profiles/skew-protection.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ platformatic:
enable: false
skew_protection:
enable: true
manage_mode: true # Grant ICC create/patch on Deployments+Services so manage-mode deploys work (testing profile)
auto_cleanup: false # Keep expired Deployments/Services for inspection
http_grace_period_ms: 120000 # 2 min: keep >= traffic_window_ms
http_max_alive_ms: 900000 # 15 min for e2e testing
Expand Down