UNPKG

@bea.steers/node-red-contrib-nuclio

Version:
248 lines (211 loc) 11.2 kB
const _ = require('lodash'); const axios = require('axios'); const { Buffer } = require('buffer'); const { diff, merge, parseIntFallback } = require('./util'); const REQUEST_TIMEOUT_MS = parseIntFallback(process.env.NUCLIO_REQUEST_TIMEOUT_MS, 10000); const DEPLOY_TIMEOUT_MS = parseIntFallback(process.env.NUCLIO_DEPLOY_TIMEOUT_MS, 60000); /* -------------------------------------------------------------------------- */ /* Nuclio-only Reconciliation */ /* -------------------------------------------------------------------------- */ async function buildFunctionConfig({ name, code, config, runtime, project, address, force, labels, annotations, env, }) { // handler definition const [runtimeBase, runtimeVersion] = runtime.split(':'); const moduleName = ( runtimeBase === 'nodejs' ? 'handler' : runtimeBase === 'shell' ? 'main.sh' : 'main' ); const funcName = ( runtimeBase === 'golang' ? 'Handler' : 'handler' ); const handler = (`${moduleName}:${funcName}`); const metadata = config?.metadata || {}; return { apiVersion: 'nuclio.io/v1', kind: 'Function', ...config, metadata: { ...metadata, name: name, labels: { ...(metadata.labels || {}), ...labels, 'nuclio.io/project-name': project || 'default', }, annotations: { ...(metadata.annotations || {}), ...annotations, 'nuclio.io/generated-by': 'node-red', // 'nuclio.io/force-redeploy': force ? 'true' : 'false', // 'nuclio.io/server-address': address, }, }, spec: { runtime: runtime, handler: handler, ...config?.spec, build: { functionSourceCode: code.trim() ? Buffer.from(code).toString('base64') : undefined, ...config?.spec?.build, }, env: [ ...(env || []), ...(config?.spec?.env || []), ], }, }; } async function deployFunctionConfig(address, { config: configBody, force, }) { const projectName = configBody?.metadata?.labels?.['nuclio.io/project-name'] || 'default'; const name = configBody?.metadata?.name; console.log(`NUCLIO Deploying function ${name}...`); /* -------------------------- Get / Create Project -------------------------- */ const existingProjects = await axios.get(`${address}/api/projects`, { timeout: REQUEST_TIMEOUT_MS }); const foundProject = Object.values(existingProjects.data || {}).find(p => p?.metadata?.name === projectName); if (!foundProject) { console.log(`NUCLIO Creating project ${projectName}...`); const resp = await axios.post(`${address}/api/projects`, { metadata: { name: projectName } }, { headers: { 'Content-Type': 'application/json' }, timeout: DEPLOY_TIMEOUT_MS }); console.log(`NUCLIO ${resp.status} ${resp.statusText}: ${resp.config.url}`); if (resp.status !== 201) { throw new Error(`Failed to create project ${projectName}: ${resp.status} ${resp.statusText}`); } } /* ----------------- Check if function already exists (GET) ----------------- */ const headers = { 'Content-Type': 'application/json', 'x-nuclio-project-name': projectName, }; let r; try { r = await axios.get(`${address}/api/functions/${name}`, { headers, timeout: REQUEST_TIMEOUT_MS }); } catch (err) { if (err.response?.status !== 404) throw err; } /* -------------------- Update Function (POST/PUT/PATCH) -------------------- */ // If it does, update it if (r && r.status === 200) { // REVIEW const state = r.data?.status?.state; // building fn can't be updated if (!(BUILDING.includes(state) || WAITING.includes(state))) { // Compare the existing function with the new one const { status, ...oldFunc } = r.data; const newFunc = merge({}, oldFunc, configBody); const diffResult = diff(newFunc, {apiVersion: newFunc.apiVersion, kind: newFunc.kind, ...oldFunc}); // const invDiffResult = diff({apiVersion: newFunc.apiVersion, kind: newFunc.kind, ...oldFunc}, newFunc); // console.log("NUCLIO Function diff:", diffResult); // console.log("oldFunc", oldFunc.spec.build.triggers); // console.log("newFunc", configBody.spec.build.triggers); // console.log("invDiffResult", invDiffResult); // No differences! if (_.isEmpty(diffResult)) { // Function isn't ready, just patch the desired state to ready if ((state !== 'ready') || force) { /* ------------------------ Set desired state (PATCH) ----------------------- */ console.log(`NUCLIO Patching function ${name}...`); r = await axios.patch(`${address}/api/functions/${name}`, { "desiredState": "ready" }, { headers, timeout: DEPLOY_TIMEOUT_MS }); console.log(`NUCLIO ${r.status} ${r.statusText}: ${r.config.url}`); } else { /* ------------------------------- Up to date. ------------------------------ */ // console.log(`NUCLIO Function ${name} is already up to date.`); } } else { /* -------------------------- Update function (PUT) ------------------------- */ // REF `skip-build`: https://github.com/nuclio/nuclio/blob/37f777a642b2176835e00e44921ed204df1dd908/pkg/functionconfig/types.go#L636 const spec = diffResult?.spec || {}; if (!(spec?.build || spec?.image || spec?.runtime) && state === 'ready') { console.debug("No build changes detected. Skipping build..."); newFunc.metadata.annotations['skip-build'] = 'true' } // Detected changes, update function console.log(`NUCLIO Updating function ${name} with changes:`, diffResult); r = await axios.put(`${address}/api/functions/${name}`, newFunc, { headers, timeout: DEPLOY_TIMEOUT_MS }); console.log(`NUCLIO ${r.status} ${r.statusText}: ${r.config.url}`); } } } /* ------------------------- Create Function (POST) ------------------------- */ else { // Function doesn't exist, create it console.log(`NUCLIO Creating function ${name}...`); r = await axios.post(`${address}/api/functions`, configBody, { headers, timeout: DEPLOY_TIMEOUT_MS }); console.log(`NUCLIO ${r.status} ${r.statusText}: ${r.config.url}`); } return r; } const deployFunction = async (node, options={}) => { if (node.redeploying && !options.force || !node.fnConfigSpec) return; node.redeploying = true; node.fnInvocationStatus = -1; node.status(STATUSES.redeploying); try { options = { ...node.fnConfigSpec, ...options }; const { force, address } = options; const configBody = await buildFunctionConfig(options); await deployFunctionConfig(address, { config: configBody, force }); } catch (err) { const r = err.response; if (r && (r.status === 404 || r.status === 502 || r.status === 503)) { // console.warn(err.status, err.code, r.status, r.code) // Function not found or server error, back off and retry node.status({ fill: "red", shape: "ring", text: `Error ${r.status}` }); return 5000; } console.error("Error while deploying function:", err); console.error(err.response?.data); node.status({ fill: "red", shape: "ring", text: err.message }); node.url = null; node.redeploying = false; } }; const WAITING = ['waitingForBuild', 'waitingForResourceConfiguration', 'waitingForScaleResourceFromZero', 'waitingForScaleResourceToZero']; const BUILDING = ['building', 'configuringResources']; // **REF: https://docs.nuclio.io/en/stable/reference/function-configuration/function-configuration-reference.html#function-state-state // **REF: https://github.com/nuclio/nuclio/blob/37f777a642b2176835e00e44921ed204df1dd908/pkg/functionconfig/types.go#L931 // State Description // ready Function is deployed successfully and ready to process events. // imported Function is imported but not yet deployed. // building Function image is being built. // waitingForResourceConfiguration Function waits for resources to be ready. For instance, in case of k8s function waits for deployment/pods and etc. // waitingForScaleResourceFromZero Function is scaling up from zero replicas. // waitingForScaleResourceToZero Function is scaling down to zero replicas. // scaledToZero Function is scaled to zero, so the number of function replicas is zero. // error An error occurred during function deployment that cannot be rectified without redeployment. // unhealthy An error occurred during function deployment, which might be resolved over time, and might require redeployment. For example, issues with insufficient resources or a missing image. const STATUSES = { ready: { fill: "green", shape: "dot", text: "" }, imported: { fill: "yellow", shape: "dot", text: "Imported" }, building: { fill: "yellow", shape: "dot", text: "Building..." }, configuringResources: { fill: "yellow", shape: "dot", text: "Configuring Resources..." }, waitingForBuild: { fill: "yellow", shape: "ring", text: "Waiting For Build..." }, waitingForResourceConfiguration: { fill: "yellow", shape: "ring", text: "Waiting For Resource Configuration..." }, waitingForScaleResourceFromZero: { fill: "yellow", shape: "ring", text: "Waiting to Scale Resource From Zero..." }, waitingForScaleResourceToZero: { fill: "yellow", shape: "ring", text: "Waiting to Scale Resource To Zero..." }, scaledToZero: { fill: "grey", shape: "dot", text: "Scaled to Zero" }, error: { fill: "red", shape: "dot", text: "Error" }, unhealthy: { fill: "red", shape: "ring", text: "Unhealthy" }, // custom readyNoDashboard: { fill: "green", shape: "ring", text: "" }, redeploying: { fill: "yellow", shape: "dot", text: "Redeploying..." }, unhealthyOk: { fill: "yellow", shape: "ring", text: "Unhealthy?" }, readyNotOk: { fill: "yellow", shape: "ring", text: "Ready?" }, }; module.exports = { deployFunction, WAITING, BUILDING, STATUSES, };