Add v2 /flows api and deploy-overwrite protection

This commit is contained in:
Nick O'Leary
2016-10-09 22:02:24 +01:00
parent c60e0d389c
commit b4be1184fd
17 changed files with 876 additions and 81 deletions

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2014, 2015 IBM Corp.
* Copyright 2014, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,13 +25,23 @@ module.exports = {
log = runtime.log;
},
get: function(req,res) {
log.audit({event: "flows.get"},req);
res.json(redNodes.getFlows());
var version = req.get("Node-RED-API-Version")||"v1";
if (version === "v1") {
log.audit({event: "flows.get",version:"v1"},req);
res.json(redNodes.getFlows().flows);
} else if (version === "v2") {
log.audit({event: "flows.get",version:"v2"},req);
res.json(redNodes.getFlows());
} else {
log.audit({event: "flows.get",version:version,error:"bad_api_version"},req);
res.status(400).json({error:"bad_api_version"});
}
},
post: function(req,res) {
var version = req.get("Node-RED-API-Version")||"v1";
var flows = req.body;
var deploymentType = req.get("Node-RED-Deployment-Type")||"full";
log.audit({event: "flows.set",type:deploymentType},req);
log.audit({event: "flows.set",type:deploymentType,version:version},req);
if (deploymentType === 'reload') {
redNodes.loadFlows().then(function() {
res.status(204).end();
@@ -41,8 +51,28 @@ module.exports = {
res.status(500).json({error:"unexpected_error", message:err.message});
});
} else {
redNodes.setFlows(flows,deploymentType).then(function() {
res.status(204).end();
var flowConfig = flows;
if (version === "v2") {
flowConfig = flows.flows;
if (flows.hasOwnProperty('rev')) {
var currentVersion = redNodes.getFlows().rev;
if (currentVersion !== flows.rev) {
//TODO: log warning
return res.status(409).json({error:"version_mismatch"});
}
}
} else if (version !== 'v1') {
log.audit({event: "flows.set",version:version,error:"bad_api_version"},req);
return res.status(400).json({error:"bad_api_version"});
}
redNodes.setFlows(flowConfig,deploymentType).then(function(flowId) {
if (version === "v1") {
res.status(204).end();
} else if (version === "v2") {
res.json({rev:flowId});
} else {
// TODO: invalid version
}
}).otherwise(function(err) {
log.warn(log._("api.flows.error-save",{message:err.message}));
log.warn(err.stack);

View File

@@ -121,12 +121,15 @@
"confirm": {
"button": {
"confirm": "Confirm deploy",
"cancel": "Cancel"
"review": "Review differences",
"cancel": "Cancel",
"merge": "Merge changes"
},
"undeployedChanges": "You have undeployed changes.\n\nLeaving this page will lose these changes.",
"improperlyConfigured": "The workspace contains some nodes that are not properly configured:",
"unknown": "The workspace contains some unknown node types:",
"confirm": "Are you sure you want to deploy?"
"confirm": "Are you sure you want to deploy?",
"conflict": "The server is running a more recent set of flows."
}
},
"subflow": {

View File

@@ -66,33 +66,47 @@ function init(runtime) {
typeEventRegistered = true;
}
}
function load() {
function loadFlows() {
return storage.getFlows().then(function(config) {
return credentials.load(config.credentials).then(function() {
return setConfig(config.flows,"load");
return config;
});
}).otherwise(function(err) {
log.warn(log._("nodes.flows.error",{message:err.toString()}));
console.log(err.stack);
});
}
function load() {
return setFlows(null,"load",false);
}
function setConfig(_config,type,muteLog) {
var config = clone(_config);
/*
* _config - new node array configuration
* type - full/nodes/flows/load (default full)
* muteLog - don't emit the standard log messages (used for individual flow api)
*/
function setFlows(_config,type,muteLog) {
type = type||"full";
var configSavePromise = null;
var config = null;
var diff;
var newFlowConfig = flowUtil.parseConfig(clone(config));
if (type !== 'full' && type !== 'load') {
diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig);
}
var newFlowConfig;
if (type === 'load') {
type = 'full';
configSavePromise = when.resolve();
if (type === "load") {
configSavePromise = loadFlows().then(function(_config) {
config = clone(_config.flows);
newFlowConfig = flowUtil.parseConfig(clone(config));
type = "full";
return _config.rev;
});
} else {
config = clone(_config);
newFlowConfig = flowUtil.parseConfig(clone(config));
if (type !== 'full') {
diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig);
}
credentials.clean(config);
var credsDirty = credentials.dirty();
configSavePromise = credentials.export().then(function(creds) {
@@ -101,18 +115,22 @@ function setConfig(_config,type,muteLog) {
credentialsDirty:credsDirty,
credentials: creds
}
storage.saveFlows(saveConfig);
return storage.saveFlows(saveConfig);
});
}
return configSavePromise
.then(function() {
activeConfig = config;
.then(function(flowRevision) {
activeConfig = {
flows:config,
rev:flowRevision
};
activeFlowConfig = newFlowConfig;
if (started) {
return stop(type,diff,muteLog).then(function() {
context.clean(activeFlowConfig);
start(type,diff,muteLog);
return flowRevision;
}).otherwise(function(err) {
})
}
@@ -143,7 +161,7 @@ function eachNode(cb) {
}
}
function getConfig() {
function getFlows() {
return activeConfig;
}
@@ -341,8 +359,8 @@ function checkTypeInUse(id) {
throw new Error(log._("nodes.index.unrecognised-id", {id:id}));
} else {
var inUse = {};
var config = getConfig();
config.forEach(function(n) {
var config = getFlows();
config.flows.forEach(function(n) {
inUse[n.type] = (inUse[n.type]||0)+1;
});
var nodesInUse = [];
@@ -418,10 +436,10 @@ function addFlow(flow) {
nodes.push(node);
}
}
var newConfig = clone(activeConfig);
var newConfig = clone(activeConfig.flows);
newConfig = newConfig.concat(nodes);
return setConfig(newConfig,'flows',true).then(function() {
return setFlows(newConfig,'flows',true).then(function() {
log.info(log._("nodes.flows.added-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"}));
return flow.id;
});
@@ -501,7 +519,7 @@ function updateFlow(id,newFlow) {
}
label = activeFlowConfig.flows[id].label;
}
var newConfig = clone(activeConfig);
var newConfig = clone(activeConfig.flows);
var nodes;
if (id === 'global') {
@@ -539,7 +557,7 @@ function updateFlow(id,newFlow) {
}
newConfig = newConfig.concat(nodes);
return setConfig(newConfig,'flows',true).then(function() {
return setFlows(newConfig,'flows',true).then(function() {
log.info(log._("nodes.flows.updated-flow",{label:(label?label+" ":"")+"["+id+"]"}));
})
}
@@ -556,12 +574,12 @@ function removeFlow(id) {
throw e;
}
var newConfig = clone(activeConfig);
var newConfig = clone(activeConfig.flows);
newConfig = newConfig.filter(function(node) {
return node.z !== id && node.id !== id;
});
return setConfig(newConfig,'flows',true).then(function() {
return setFlows(newConfig,'flows',true).then(function() {
log.info(log._("nodes.flows.removed-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"}));
});
}
@@ -581,7 +599,7 @@ module.exports = {
/**
* Gets the current flow configuration
*/
getFlows: getConfig,
getFlows: getFlows,
/**
* Sets the current active config.
@@ -589,7 +607,7 @@ module.exports = {
* @param type the type of deployment to do: full (default), nodes, flows, load
* @return a promise for the saving/starting of the new flow
*/
setFlows: setConfig,
setFlows: setFlows,
/**
* Starts the current flow configuration

View File

@@ -16,6 +16,8 @@
var when = require('when');
var Path = require('path');
var crypto = require('crypto');
var log = require("../log");
var runtime;
@@ -57,10 +59,12 @@ var storageModuleInterface = {
getFlows: function() {
return storageModule.getFlows().then(function(flows) {
return storageModule.getCredentials().then(function(creds) {
return {
var result = {
flows: flows,
credentials: creds
}
};
result.rev = crypto.createHash('md5').update(JSON.stringify(result)).digest("hex");
return result;
})
});
},
@@ -73,9 +77,12 @@ var storageModuleInterface = {
} else {
credentialSavePromise = when.resolve();
}
delete config.credentialsDirty;
return credentialSavePromise.then(function() {
return storageModule.saveFlows(flows);
return storageModule.saveFlows(flows).then(function() {
return crypto.createHash('md5').update(JSON.stringify(config)).digest("hex");
})
});
},
// getCredentials: function() {