@bea.steers/node-red-contrib-nuclio
Version:
Deploy Nuclio functions from Node-RED
680 lines (594 loc) • 29.3 kB
HTML
<style>
.node_label_monospace {
font-family: monospace;
font-size: 0.82em;
font-weight: 900;
}
</style>
<!-- ----------------------------------------------------------------------- -->
<!-- Nuclio Server -->
<!-- ----------------------------------------------------------------------- -->
<script type="text/javascript">
RED.nodes.registerType('nuclio-config', {
category: 'config',
icon: "nuclio-logo.svg",
defaults: {
address: { value: 'NUCLIO_ADDRESS', required: true },
addressType: { value: 'env' },
publicAddress: { value: 'http://localhost:8070', required: false },
publicAddressType: { value: 'str' },
},
label: function () {
return this.publicAddress || this.address || 'Nuclio Server';
},
oneditprepare: function () {
$('#node-config-input-address').typedInput({
default: this.addressType,
types: ['str', 'global', 'env'],
});
$('#node-config-input-publicAddress').typedInput({
default: this.publicAddressType,
types: ['str', 'global', 'env'],
});
},
});
</script>
<script type="text/html" data-template-name="nuclio-config">
<div class="form-row">
<label for="node-config-input-address">
<span>Nuclio Dashboard URL:</span>
</label>
<input type="text" id="node-config-input-address" placeholder="http://localhost:8070" />
<div>
<small>The address that node-red can reach nuclio at. (typically port 8070)</small>
</div>
</div>
<div class="form-row">
<label for="node-config-input-publicAddress">
<span>Public Nuclio Dashboard URL (optional)</span>
</label>
<input type="text" id="node-config-input-publicAddress" placeholder="http://localhost:8070" />
<div>
<small>The address that YOU (and your browser) can reach nuclio by. This is just used for links to the nuclio dashboard and is not critical to operation. This could just be your local port forward address for simplicity.</small>
</div>
</div>
</script>
<!-- ----------------------------------------------------------------------- -->
<!-- Nuclio Project -->
<!-- ----------------------------------------------------------------------- -->
<script type="text/javascript">
RED.nodes.registerType('nuclio-project', {
category: 'config',
icon: "nuclio-logo.svg",
defaults: {
name: { value: 'default' },
nameType: { value: 'str' },
},
label: function () {
return this.name || 'default';
},
oneditprepare: function () {
$('#node-project-input-name').typedInput({
default: this.nameType,
types: ['str', 'global', 'env'],
});
},
});
</script>
<script type="text/html" data-template-name="nuclio-project">
<div class="form-row">
<label for="node-project-input-name">
<span>Nuclio Project Name</span>
</label>
<input type="text" id="node-project-input-name" placeholder="default" />
</div>
</script>
<!-- ----------------------------------------------------------------------- -->
<!-- Nuclio Function -->
<!-- ----------------------------------------------------------------------- -->
<script type="text/javascript">
const sampleList = (list) => list[Math.floor(Math.random() * list.length)];
const randomEndpoint = () => {
const adjectives = ['adaptive', 'agile', 'airy', 'amber', 'amplified', 'blazing', 'blissful', 'boundless', 'breezy', 'bright', 'buoyant', 'calm', 'caressing', 'cascading', 'celestial', 'cloudy', 'cocooned', 'cosmic', 'crystalline', 'dappled', 'dazzling', 'dewlit', 'digital', 'dreamy', 'drifting', 'dynamic', 'elastic', 'electric', 'elegant', 'endless', 'ethereal', 'evolving', 'feathered', 'fluent', 'futuristic', 'gentle', 'gentlehearted', 'glassy', 'global', 'glowing', 'gossamer', 'graceful', 'gravitational', 'harmonic', 'harmonious', 'hazy', 'hushed', 'hyper', 'infinite', 'innovative', 'interstellar', 'iridescent', 'joyful', 'kindred', 'kinetic', 'lavender', 'lilac', 'lilting', 'lucent', 'luminous', 'lunar', 'magnetic', 'mellow', 'melodic', 'mighty', 'misty', 'modular', 'moonlit', 'nanotech', 'nebula', 'neural', 'oblique', 'omniscient', 'opalescent', 'orbital', 'orbiting', 'pastel', 'peachy', 'pearl', 'perennial', 'petal', 'plasma', 'plush', 'pulsating', 'quantum', 'radiant', 'reactive', 'revolutionary', 'rippling', 'roselight', 'rosy', 'seamless', 'serene', 'shimmering', 'shiny', 'silken', 'silver', 'silvery', 'smooth', 'soaring', 'soft', 'solar', 'sonic', 'soothing', 'sparkling', 'spectral', 'spun', 'stellar', 'sunny', 'supercharged', 'sweet', 'swift', 'tranquil', 'transcendent', 'turbo', 'twinkling', 'ultra', 'unified', 'velvet', 'velvety', 'vibrant', 'virtual', 'volatile', 'warm', 'wavy', 'weightless', 'whimsical', 'wistful', 'zealous', 'zippy'];
const nouns = ['accelerator', 'api', 'array', 'aura', 'backbone', 'band', 'beacon', 'bloom', 'breeze', 'bubble', 'cartridge', 'channel', 'chip', 'circuit', 'cluster', 'cocoon', 'conduit', 'core', 'dawn', 'domain', 'dream', 'drift', 'dusk', 'echo', 'endpoint', 'engine', 'fabric', 'feather', 'flame', 'flow', 'flux', 'gateway', 'gleam', 'glean', 'glimmer', 'glow', 'grid', 'halo', 'harmony', 'haze', 'hive', 'horizon', 'hub', 'interface', 'kernel', 'lattice', 'light', 'link', 'logic', 'lotus', 'lull', 'matrix', 'mesh', 'mist', 'module', 'murmur', 'muse', 'nest', 'net', 'nexus', 'node', 'note', 'opal', 'orb', 'oscillator', 'path', 'pathway', 'petal', 'pipeline', 'plane', 'point', 'portal', 'processor', 'protocol', 'pulse', 'radar', 'reactor', 'relay', 'ribbon', 'ripple', 'rose', 'route', 'scaffold', 'scent', 'segment', 'sequence', 'service', 'shade', 'shell', 'shimmer', 'sky', 'song', 'spark', 'spectrum', 'sphere', 'stream', 'switch', 'system', 'terrain', 'thread', 'tide', 'token', 'tone', 'topology', 'trace', 'trail', 'twilight', 'vector', 'veil', 'vertex', 'vibe', 'warp', 'wave', 'wavelength', 'web', 'webbing', 'whisper', 'wind', 'wing', 'wire', 'wisp', 'zephyr', 'zone'];
return `${sampleList(adjectives)}-${sampleList(nouns)}`;
}
const configCode = `
apiVersion: "nuclio.io/v1"
kind: NuclioFunction
metadata:
labels: {}
annotations: {}
spec:
build:
commands: []
# - pip install msgpack # currently required for python fns on Mac
# - pip install requests numpy pandas
# # add multiple workers:
# triggers:
# mh:
# kind: http
# numWorkers: 4
#############
# Kubernetes:
# env: []
# envFrom: []
# volumes: []
# # Pod replica auto-scaling:
# minReplicas: 1
# maxReplicas: 1
# resources: {}
# # Kubernetes Limits & Requests for the function's CPU and memory usage.
# requests:
# cpu: 1
# memory: 128M
# limits:
# cpu: 2
# memory: 256M
# nvidia.com/gpu: 1
`.trim();
const SAMPLES = {
python: {
config: configCode,
entrypoint: 'handler',
code: `
import nuclio_sdk
async def handler(context: nuclio_sdk.Context, event: nuclio_sdk.Event):
# reverse the input, return JSON
return {
"some-output": event.body[::-1],
"hi": "Hello, from Nuclio :]",
}
`.trim()
},
golang: {
config: configCode,
entrypoint: 'Handler',
code: `
package main
import (
"github.com/nuclio/nuclio-sdk-go"
)
func Handler(context *nuclio.Context, event nuclio.Event) (interface{}, error) {
context.Logger.Info("This is an unstructured %s", "log")
return nuclio.Response{
StatusCode: 200,
ContentType: "application/text",
Body: []byte("Hello, from Nuclio :]"),
}, nil
}
`.trim()
},
nodejs: {
config: configCode,
entrypoint: 'handler',
code: `
exports.handler = function(context, event) {
var body = event.body.toString();
context.logger.info('reversing: ' + body);
context.callback(body.split('').reverse().join(''));
};
`.trim()
},
shell: {
config: configCode,
code: `
#!/bin/sh
# reverse the input
rev /dev/stdin
`.trim()
},
}
const getUniqueValues = (node, key) => {
let endpoints = {};
RED.nodes.eachNode(n => {
if(n.type !== "nuclio" || !n.server || n.server !== node.server || n.project !== node.project || n.id === node.id) return;
endpoints[key(n)] = n.id;
})
return endpoints;
}
const cleanRouteName = (route_prefix) => route_prefix.replace(/^\//g, '').replace(/\//g, ' ');
const nodeName = (n) => n.name || cleanRouteName(n.route_prefix);
class Poller {
constructor(fn, interval) {
this.fn = fn;
this.interval = interval;
this.pollInterval = null;
}
start() {
this.fn();
this.pollInterval = setInterval(this.fn, this.interval);
}
stop() {
clearInterval(this.pollInterval);
}
}
const fmt = (obj) => Object.entries(obj).map(([k,v]) => {
if (typeof v === 'string' && v.includes('\n')) {
return `<b>${k}</b>: <div style="padding-left: 12px;">${v}</div>`;
}
return `<b>${k}</b>: ${JSON.stringify(v)}`
}).join('\n');
// const yamlFmt = (obj) => Object.entries(obj).map(([k,v]) => `<b>${k}</b>: ${yamlFmt(v)}`).join('\n');
RED.nodes.registerType('nuclio', {
category: 'function',
color: '#65a9fc',
inputs: 1,
outputs: 2,
outputLabels: ['result', 'error/backpressure'],
icon: "nuclio.io.ico",
defaults: {
server: {
type: 'nuclio-config',
required: false,
},
project: {
type: 'nuclio-project',
required: false,
},
name: { value: '', validate: function(v) {
if(!v) {
v = randomEndpoint();
let existing = getUniqueValues(this, n=>n.name);
while (existing[v]) {
v = randomEndpoint();
}
this.name = v;
}
return !getUniqueValues(this, n=>n.name)[v];
} },
runtime: { value: 'python:3.11' },
code: { value: SAMPLES.python.code },
configCode: { value: SAMPLES.python.config },
env_vars: { value: [] },
secret_vars: { value: [] },
},
label: function () {
return this.name || 'nuclio fn';
},
oneditprepare: function() {
let node = this;
node.originalName = node.name;
// <!-- ----------------------------------------------------------------------- -->
// <!-- Buttons / Links -->
// <!-- ----------------------------------------------------------------------- -->
$("#nuclio-dashboard-link").on('pointerdown', function () {
let name = $('#node-input-name').val();
if (!name) return;
let project = 'default';
let server = RED.nodes.node(node.server);
let url = server?.publicAddress || server?.address || 'http://localhost:8070';
let fullUrl = `${url}/projects/${project}/functions/${name}/code`;
this.href = fullUrl;
});
$('#nuclio-redeploy').on('click', function() {
$.post(`/nuclio/api/functions/deploy?id=${node.id}`, function(data) {
console.log(data);
}).fail((e) => {
alert('Error redeploying: ' + JSON.stringify(e));
});
})
$('#nuclio-generate-name').on('click', function() {
let name = randomEndpoint();
let existing = getUniqueValues(node, n=>n.name);
while (existing[name]) {
name = randomEndpoint();
}
$('#node-input-name').val(name);
});
// <!-- ----------------------------------------------------------------------- -->
// <!-- Code Editors -->
// <!-- ----------------------------------------------------------------------- -->
const runtime = node.runtime || 'python:3.11';
const [runtimeBase, runtimeVersion] = runtime.split(':');
const lang = {
python: 'python',
golang: 'go',
nodejs: 'javascript',
shell: 'shell',
}[runtimeBase];
this.editor = RED.editor.createEditor({
id: 'node-input-nuclio-editor',
mode: `ace/mode/${lang}`,
value: this.code || "",
// height: '700px',
// stateId: stateId,
focus: true,
// extraLibs: extraLibs
});
this.configeditor = RED.editor.createEditor({
id: 'node-input-nuclio-config-editor',
mode: `ace/mode/yaml`,
value: this.configCode || "",
// height: '700px',
// stateId: stateId,
focus: true,
// extraLibs: extraLibs
});
$('#node-input-runtime').on('change', function() {
const runtime = $(this).val();
const [runtimeBase, runtimeVersion] = runtime.split(':');
const lang = {
python: 'python',
golang: 'go',
nodejs: 'javascript',
shell: 'shell',
}[runtimeBase];
console.log("Changing runtime & language", runtime, lang);
let code = node.editor.getValue();
let configCode = node.configeditor.getValue();
const codes = Object.values(SAMPLES).map(v => v.code);
const configCodes = Object.values(SAMPLES).map(v => v.config);
if(code.trim() === '' || codes.includes(code)) {
code = SAMPLES[runtimeBase].code;
node.editor.setValue(code);
}
if(configCode.trim() === '' || configCodes.includes(configCode)) {
node.configeditor.setValue(SAMPLES[runtimeBase].config);
}
node.editor.setMode(`ace/mode/${lang}`);
});
// <!-- ----------------------------------------------------------------------- -->
// <!-- Settings -->
// <!-- ----------------------------------------------------------------------- -->
this.envList = $("#node-input-env_vars-x").editableList({
addItem: function(row, index, data) {
$(`<input class='node-input-key' type="text" placeholder="VAR_NAME" />`).appendTo(row).val(data.name)
$(`<input class='node-input-value' placeholder="value" />`).appendTo(row).attr('value', data.value).typedInput({
defaultType: data.type || 'str',
types: ['str','num','json','env','cred'],
});
},
removable: true,
});
for(let data of this.env_vars || []) {
this.envList.editableList('addItem', data)
}
// TODO: add secrets/overrides support e.g. with `build.codeEntryType: "github"`
// build.codeEntryAttributes.headers.Authorization: "my-GitHub-access-token" (typedInput: cred/env)
this.secretList = $("#node-input-secret_vars-x").editableList({
addItem: function(row, index, data) {
$(`<input class='node-input-key' type="text" placeholder="VAR_NAME" />`).appendTo(row).val(data.name)
$(`<input class='node-input-value' placeholder="value" />`).appendTo(row).attr('value', data.value).typedInput({
defaultType: data.type || 'cred',
types: ['env','cred'],
});
},
removable: true,
});
for(let data of this.secret_vars || []) {
this.secretList.editableList('addItem', data)
}
// <!-- ----------------------------------------------------------------------- -->
// <!-- Status Page -->
// <!-- ----------------------------------------------------------------------- -->
this.statusPoller = new Poller(() => {
$.getJSON(`/nuclio/api/functions?id=${this.id}`, function (data) {
const { status, metadata, spec } = data;
const { logs, ...statusMeta } = status;
const state = status.state;
$('#nuclio-status').html(fmt(statusMeta));
$('#nuclio-deploy-logs').html(logs?.map(({ level, message, time, name, requestID, ...log }) => //builderKind, versionInfo,
`<div><b>${level}</b> ${message} ${Object.keys(log).length ? `\n<div style="padding-left: 12px;">${fmt(log)}</div>` : ''}</div>`).join('\n'));
$('#nuclio-spec').text(JSON.stringify({ metadata, spec }, null, 2));
$('.red-ui-tab-label[href="#nuclio-tab-status"]').html(`Status: <span style="color: ${state === 'ready' ? 'green' : (state||'').includes('waiting') ? 'yellow' : 'red'}">${state}</span>`);
}).fail((e) => {
$('#nuclio-status').text(JSON.stringify(e, null, 2));
});
$.getJSON(`/nuclio/api/functions/logs?id=${this.id}`, function (data) {
// $('#nuclio-logs').text(JSON.stringify(data, null, 2));
$('#nuclio-logs').html(Object.entries(data||{}).map(([replica, log]) => //builderKind, versionInfo,
`<div><b>${replica}</b> \n<div style='padding-left: 12px'>${log || '-- no logs --'}</div></div>`).join('\n'));
}).fail((e) => {
$('#nuclio-logs').text(JSON.stringify(e, null, 2));
});
}, 2400);
this.statusPoller.start();
// <!-- ----------------------------------------------------------------------- -->
// <!-- Tabs -->
// <!-- ----------------------------------------------------------------------- -->
var tabs = RED.tabs.create({
id: "nuclio-tabs",
onchange: function(tab) {
$("#nuclio-tabs-content").children().hide();
$("#" + tab.id).show();
}
});
tabs.addTab({
id: "nuclio-tab-app",
label: "Code"
});
tabs.addTab({
id: "nuclio-tab-deps",
label: "Config"
});
tabs.addTab({
id: "nuclio-tab-status",
label: "Status"
});
tabs.activateTab("nuclio-tab-app");
},
oneditsave: function() {
let node = this;
this.code = this.editor.getValue();
this.editor.destroy();
delete this.editor;
this.configCode = this.configeditor.getValue();
this.configeditor.destroy();
delete this.configeditor;
this.statusPoller.stop();
delete this.statusPoller;
node.env_vars = [];
$("#node-input-env_vars-x").editableList('items').each(function(i) {
let name = $(this).find(".node-input-key").val();
let type = $(this).find(".node-input-value").typedInput('type');
let value = $(this).find(".node-input-value").typedInput('value');
name && node.env_vars.push({ name, type, value });
})
delete this.envList;
node.secret_vars = [];
$("#node-input-secret_vars-x").editableList('items').each(function(i) {
let name = $(this).find(".node-input-key").val();
let type = $(this).find(".node-input-value").typedInput('type');
let value = $(this).find(".node-input-value").typedInput('value');
name && node.secret_vars.push({ name, type, value });
})
delete this.secretList;
},
oneditcancel: function() {
this.editor.destroy();
delete this.editor;
this.configeditor.destroy();
delete this.configeditor;
this.statusPoller.stop();
delete this.statusPoller;
this.name = this.originalName;
delete this.envList;
delete this.secretList;
},
});
</script>
<!--
-----------------------------------------------------------------------
Form UI
-----------------------------------------------------------------------
-->
<script type="text/html" data-template-name="nuclio">
<style>
#nuclio-tabs-content {
height: 100%;
min-height: 350px;
max-height: calc(100% - 95px);
}
#nuclio-tabs-content > * {
height: 100%;
display: flex;
flex-direction: column;
justify-content: stretch;
}
#nuclio-tabs-content .node-text-editor {
flex-grow: 1;
}
/* Editable List */
#nuclio-tabs-content .node-input-list-row > label {
/* font-size: 1em; */
/* font-weight: 700; */
margin: 12px 0 0 4px;
width: 100%;
}
#nuclio-tabs-content .red-ui-editableList-item-content {
display: flex;
justify-content: stretch;
}
#nuclio-tabs-content .red-ui-editableList-item-content > * {
flex-grow: 1;
}
#nuclio-tabs-content .red-ui-editableList-container input.node-input-key {
flex-basis: 100px;
flex-grow: 0;
}
#node-input-env_vars-x .red-ui-typedInput-container,
#nuclio-tabs-content .node-input-list-row .red-ui-editableList-container input {
border-radius: 0;
}
#nuclio-tabs-content .red-ui-editableList-container input {
width: auto;
}
</style>
<div class="form-row nuclio-tabs-row">
<ul style="min-width: 600px; margin-bottom: 20px;" id="nuclio-tabs"></ul>
</div>
<div id="nuclio-tabs-content" style="min-height: calc(100% - 25px);">
<div id="nuclio-tab-app" style="display:none">
<div class="form-row">
<p>
<h2 style="display: inline-block;">
<a target="_blank" href="https://docs.nuclio.io/en/stable/index.html" title="">Nuclio</a>
</h2>
<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/python/python-reference.html">Python</a>
<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/golang/golang-reference.html">Go</a>
<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/nodejs/nodejs-reference.html">NodeJS</a>
<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/shell/shell-reference.html">Shell</a>
<a target="_blank" href="https://docs.nuclio.io/en/stable/examples/README.html">Examples</a>
<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/function-configuration/batching.html" title="Request batching to handle multiple requests at once.">Batching</a>
<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/function-configuration/function-configuration-reference.html" title="">Ref.</a>
</p>
</div>
<div class="form-row">
<div>
<label for="node-input-name">
<span>Function Name</span>
</label>
<input type="text" id="node-input-name" placeholder="What should the URL endpoint be?" title="This is the URL that nodered will call to run this function." />
<!-- <small class="name-error">Name must be unique.</small> -->
<button id="nuclio-generate-name" class="btn btn-default" title="Generate a random name">🔀</button>
</div>
</div>
<div class="form-row">
<div>
<label for="node-input-runtime">
<span>Runtime [<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/index.html" title="">docs</a>]:</span></span>
</label>
<select id="node-input-runtime" class="form-control">
<!-- <option value="python:3.12">python:3.12</option> -->
<option value="python:3.11">python:3.11 (entrypoint: handler)</option>
<option value="python:3.10">python:3.10 (entrypoint: handler)</option>
<option value="python:3.9">python:3.9 (entrypoint: handler)</option>
<option value="golang">golang (entrypoint: Handler)</option>
<option value="nodejs">nodejs (entrypoint: handler)</option>
<option value="shell">shell (stdin/stdout)</option>
<!-- <option value="ruby">Ruby</option> -->
</select>
</div>
</div>
<div style="min-height:150px;" class="node-text-editor" id="node-input-nuclio-editor" ></div>
</div>
<div id="nuclio-tab-deps" style="display:none">
<div class="form-row">
<label for="node-input-server">Server</label>
<input type="text" id="node-input-server" />
<!-- <div>
<small>What Nuclio Server should this deploy to?</small>
</div> -->
</div>
<div class="form-row">
<label for="node-input-project">
<span>Project</span>
</label>
<input type="text" id="node-input-project" placeholder="default" />
</div>
<div class="form-row">
<h2>Function Config [<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/function-configuration/function-configuration-reference.html" title="">docs</a>]:</h2>
<p>
</p>
</div>
<div style="min-height:150px;" class="node-text-editor" id="node-input-nuclio-config-editor" ></div>
<div class="form-row node-input-list-row">
<label for="node-input-env_vars-x">
Environment Variables
</label>
<p>
<small>You can specify environment variables that will be available to your application at runtime.</small>
</p>
<ol id="node-input-env_vars-x"></ol>
</div>
<div class="form-row node-input-list-row">
<label for="node-input-secret_vars-x">
Secret Overrides
</label>
<p>
<small>You can specify secret variables (e.g. build.codeEntryAttributes.s3SecretAccessKey: my-53cr3t-@cce55-k3y)</small>
</p>
<ol id="node-input-secret_vars-x"></ol>
</div>
</div>
<div id="nuclio-tab-status" style="display:none">
<p>Status: <a id="nuclio-dashboard-link" href="about:blank" target="_blank" rel="noopener noreferrer">Dashboard</a> <button id="nuclio-redeploy">Redeploy</button></p><pre><code id="nuclio-status"></code></pre>
<p>Run Logs:</p><pre><code id="nuclio-logs"></code></pre>
<p>Build Logs:</p><pre><code id="nuclio-deploy-logs"></code></pre>
<p>Spec:</p><pre><code id="nuclio-spec"></code></pre>
</div>
</div>
</script>
<script type="text/html" data-help-name="nuclio">
</script>