Skip to content

Instantly share code, notes, and snippets.

@gkousiouris
Last active November 16, 2023 02:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gkousiouris/7970809f43d73b0a2e7af27f7165420c to your computer and use it in GitHub Desktop.
Save gkousiouris/7970809f43d73b0a2e7af27f7165420c to your computer and use it in GitHub Desktop.
Dynamic OW Orchestrator Action

The goal of this subflow is to enable the use of dynamically located Openwhisk action invocations in hybrid cloud/edge or multi-cloud scenarios, in which the location of the target function is not known at design time.

Screenshot 2023-09-15 193648

Its main operation is rather simple, it exploits incoming information from the message in order to adapt the url to which the function invocation will be performed. The incoming message to the Dynamic OW Action needs to include the action name in the node UI as well as the following fields in the msg.payload.value:

-__<action_name>_HOST

-__<action_name>_NAMESPACE

-__<action_name>_CREDS

The subflow includes also the PollToPushConverterFC, so that it directly returns the final result of the invoked function for the continuation of the workflow. By using that subflow, the developer may create an arbitrary workflow of various used actions, linked in whatever manner they see fit.

The subflow is wrapped around the Openwhisk function interface, so that it can be executed as an Openwhisk action.

Screenshot 2023-09-15 193556

To enable its default passing of dynamic action information, one of Openwhisk’s features may be exploited, i.e. the ability to define parameters to a registered function upon creation or update. An example of such a registration appears in the following snippet:

wsk action update -i <ORCHESTRATOR FUNCTION NAME> -p __<DYNAMIC ACTION NAME>_HOST    <OW-ENDPOINT>/api/v1/  -p __<DYNAMIC ACTION NAME>_NAMESPACE <NAMESPACE-NAME> -p  __<DYNAMIC ACTION NAME>_CREDS user:token
[
{
"id": "789351ac.02c9d",
"type": "subflow",
"name": "PollTOPushConverterFC",
"info": "This helper node aims at performing synchronous calls for polling in case of an async API (calls that return prior to completion, e.g. in the case of non blocking calls in OW, the initial request is returned with a submission success and an activation id in order to follow up on the result). The main difference from the main PollTOPushConverter is that it supports also function chaining (through a msg.functionChain or UI set boolean parameter).\n\nThe according function should return as a response the following structure:\n- have a result.status field with the 'Completed' (if chain is finished) or 'Continuing' string if the function has handed over to a new function in the chain.\n- have a result.newActivationIDURL field that indicates the updated ID with which to check the follow-up action's status \n\nThe node also can get credentials from the msg.creds field of the input message.\n\nThe remaining operations are similar with the standard PollTOPushConverter. The node does polling to a specific endpoint in order to detect whether the function has successfully finished\n\nThe node has three outputs:\n - Output 1 indicates successful finalization of the API call\n - Output 2 indicates intermediate failure and was added to return the reason for failure\n - Output 3 indicates final failure after max attempts\n\nCurrently the node assumes that in case of failure we get a >=40X return code, but this may not always be the case.\n\nThe node can be configured for the URL (msg.url), the HTTP method (msg.method), the maximum attempts (msg.maxAttempts), polling period (msg.pollPeriod), the status code above which to retry (msg.retryCode) and the status code for deciding the final success (msg.acceptCode). The msg properties override the UI set properties.\n\nGiven that conditions upon which the initial call needs to be polled are highly dependent on the used API, the node assumes that the initial call has been performed a priori.\n\nCredentials for accessing the HTTP endpoint can be set via msg.creds or through the UI (input msg prevails).\n \n",
"category": "PHYSICS Helpers",
"in": [
{
"x": 60,
"y": 160,
"wires": [
{
"id": "7d194c10.33b51c"
}
]
}
],
"out": [
{
"x": 1380,
"y": 40,
"wires": [
{
"id": "f9d0aba7.72382",
"port": 0
},
{
"id": "1bbc6fa0.4879c",
"port": 0
}
]
},
{
"x": 780,
"y": 260,
"wires": [
{
"id": "c2cf68b7.263d4",
"port": 0
}
]
},
{
"x": 780,
"y": 360,
"wires": [
{
"id": "c2cf68b7.263d4",
"port": 1
}
]
}
],
"env": [
{
"name": "maxAttempts",
"type": "str",
"value": "3"
},
{
"name": "pollPeriod",
"type": "str",
"value": "3000"
},
{
"name": "method",
"type": "str",
"value": ""
},
{
"name": "url",
"type": "str",
"value": "http://10.100.59.182:3233/api/v1/namespaces/_/activations/"
},
{
"name": "retryCode",
"type": "num",
"value": "202"
},
{
"name": "acceptCode",
"type": "num",
"value": "200"
},
{
"name": "functionChain",
"type": "bool",
"value": "true"
}
],
"meta": {},
"color": "#b4e8a9"
},
{
"id": "7d194c10.33b51c",
"type": "function",
"z": "789351ac.02c9d",
"name": "defaults+activation id",
"func": "\nif (msg.hasOwnProperty('maxAttempts')){\n msg.iterations=msg.maxAttempts;\n} else {\n msg.iterations=env.get('maxAttempts');\n}\n//needed to reset the iterations after function chaining\nmsg.defaultAttempts=msg.iterations;\n\nif (msg.hasOwnProperty('pollPeriod')){\n \n} else {\n msg.pollPeriod=env.get('pollPeriod');\n}\n\nif (msg.hasOwnProperty('method')){\n \n} else {\n msg.method=env.get('method');\n}\n\nif (msg.hasOwnProperty('url')){\n \n} else {\n msg.url=env.get('url');\n}\n\nif (msg.hasOwnProperty('retryCode')){\n \n} else {\n msg.retryCode=env.get('retryCode');\n}\n\nif (msg.hasOwnProperty('acceptCode')){\n \n} else {\n msg.acceptCode=env.get('acceptCode');\n}\n\nif (msg.hasOwnProperty('creds')){\n \n} else {\n msg.creds=env.get('creds');\n}\n\nif (msg.hasOwnProperty('functionChain')){\n \n} else {\n msg.functionChain=env.get('functionChain');\n}\n\nif (msg.hasOwnProperty('inputData')){\n if (msg.inputData.hasOwnProperty('creds')){\n msg.creds=msg.inputData.creds;\n }\n}\n\n\n\nmsg.delay=msg.pollPeriod;\nmsg.start=Date.now();\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 200,
"y": 160,
"wires": [
[
"cad17b04.a2b4d8"
]
]
},
{
"id": "4be3f205.3e8dc4",
"type": "function",
"z": "789351ac.02c9d",
"name": "iterations--",
"func": "msg.iterations=msg.iterations-1;\n/*\nif (flow.get('stop')==true){\n msg.iterations=-1;\n}*/\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 350,
"y": 300,
"wires": [
[
"c2cf68b7.263d4"
]
]
},
{
"id": "c2cf68b7.263d4",
"type": "switch",
"z": "789351ac.02c9d",
"name": "if iterations finished",
"property": "iterations",
"propertyType": "msg",
"rules": [
{
"t": "gte",
"v": "1",
"vt": "num"
},
{
"t": "lt",
"v": "1",
"vt": "num"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 550,
"y": 300,
"wires": [
[
"cad17b04.a2b4d8"
],
[]
]
},
{
"id": "76299695.812bd8",
"type": "comment",
"z": "789351ac.02c9d",
"name": "SUCCESS",
"info": "",
"x": 1100,
"y": 60,
"wires": []
},
{
"id": "48093072.b17a6",
"type": "comment",
"z": "789351ac.02c9d",
"name": "FINAL FAIL",
"info": "",
"x": 890,
"y": 360,
"wires": []
},
{
"id": "d5960f2f.2e9448",
"type": "delay",
"z": "789351ac.02c9d",
"name": "delay",
"pauseType": "delayv",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"x": 170,
"y": 300,
"wires": [
[
"4be3f205.3e8dc4"
]
]
},
{
"id": "471b3d64.265894",
"type": "http request",
"z": "789351ac.02c9d",
"name": "",
"method": "use",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "2934f390.711324",
"persist": false,
"proxy": "",
"authType": "",
"x": 590,
"y": 160,
"wires": [
[
"8694180b.ec1c68"
]
]
},
{
"id": "8694180b.ec1c68",
"type": "switch",
"z": "789351ac.02c9d",
"name": "Status code check",
"property": "statusCode",
"propertyType": "msg",
"rules": [
{
"t": "lte",
"v": "acceptCode",
"vt": "msg"
},
{
"t": "gte",
"v": "retryCode",
"vt": "msg"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 810,
"y": 160,
"wires": [
[
"f9d0aba7.72382"
],
[
"d5960f2f.2e9448"
]
]
},
{
"id": "ef03414f.ad9d78",
"type": "comment",
"z": "789351ac.02c9d",
"name": "RETRY FAIL",
"info": "",
"x": 890,
"y": 260,
"wires": []
},
{
"id": "66bbc0cf.0c62f",
"type": "debug",
"z": "789351ac.02c9d",
"name": "BEFORE AUTH",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 370,
"y": 100,
"wires": []
},
{
"id": "13791c2d.8cea7c",
"type": "debug",
"z": "789351ac.02c9d",
"name": "URL",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 580,
"y": 100,
"wires": []
},
{
"id": "cad17b04.a2b4d8",
"type": "function",
"z": "789351ac.02c9d",
"name": "add auth",
"func": "msg.headers={};\nvar auth = 'Basic ' + new Buffer(msg.creds).toString('base64');\nmsg.headers = {\n \"Authorization\": auth\n}\nmsg.method='GET';\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 160,
"wires": [
[
"471b3d64.265894",
"13791c2d.8cea7c"
]
]
},
{
"id": "f2b7c4e4.a24c3",
"type": "function",
"z": "789351ac.02c9d",
"name": "adapt to new activation",
"func": "\n\nmsg.url=msg.payload.response.result.newActivationIDURL;\nmsg.iterations=msg.defaultAttempts+1;\nconsole.log('IN ADAPTATION TO ACTIVATION',msg.url);\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1500,
"y": 160,
"wires": [
[
"d5960f2f.2e9448",
"ee1dbf44.7368f8"
]
]
},
{
"id": "c338e47b.b00ed",
"type": "comment",
"z": "789351ac.02c9d",
"name": "FUNCTION CHAIN CONTINUATION",
"info": "",
"x": 1240,
"y": 240,
"wires": []
},
{
"id": "ee1dbf44.7368f8",
"type": "debug",
"z": "789351ac.02c9d",
"name": "AFTER CONTINUATION IN Poll2Push",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1420,
"y": 300,
"wires": []
},
{
"id": "f9d0aba7.72382",
"type": "switch",
"z": "789351ac.02c9d",
"name": "is Chain",
"property": "functionChain",
"propertyType": "msg",
"rules": [
{
"t": "false"
},
{
"t": "true"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 1020,
"y": 160,
"wires": [
[],
[
"6da1eea0.969368",
"1bbc6fa0.4879c"
]
]
},
{
"id": "6da1eea0.969368",
"type": "debug",
"z": "789351ac.02c9d",
"name": "RESULT STATUS",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1100,
"y": 300,
"wires": []
},
{
"id": "f20a68d.9711598",
"type": "debug",
"z": "789351ac.02c9d",
"name": "AFTER CODE CHECK",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "functionChain",
"targetType": "msg",
"x": 890,
"y": 100,
"wires": []
},
{
"id": "36905e62.dfad92",
"type": "debug",
"z": "789351ac.02c9d",
"name": "TOP1",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1120,
"y": 100,
"wires": []
},
{
"id": "9610a737.bf275",
"type": "debug",
"z": "789351ac.02c9d",
"name": "TOP2",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1300,
"y": 100,
"wires": []
},
{
"id": "c52af08f.756ee8",
"type": "debug",
"z": "789351ac.02c9d",
"name": "TOP3",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1600,
"y": 100,
"wires": []
},
{
"id": "1bbc6fa0.4879c",
"type": "function",
"z": "789351ac.02c9d",
"name": "compare completion",
"func": "console.log('IN COMPARE COMPLETION',msg.payload.response);\nif (msg.payload.response.result.status==='Completed'){\n return [msg,null];\n} else {\n return [null,msg];\n}\n",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1240,
"y": 160,
"wires": [
[
"c52af08f.756ee8"
],
[
"f2b7c4e4.a24c3"
]
]
},
{
"id": "d8305759.866988",
"type": "debug",
"z": "789351ac.02c9d",
"name": "URL",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "iterations",
"targetType": "msg",
"x": 500,
"y": 380,
"wires": []
},
{
"id": "2934f390.711324",
"type": "tls-config",
"z": "789351ac.02c9d",
"name": "",
"cert": "",
"key": "",
"ca": "",
"certname": "",
"keyname": "",
"caname": "",
"servername": "",
"verifyservercert": false
},
{
"id": "3352c7dddc9aad6d",
"type": "subflow",
"name": "Dynamic OW action",
"info": "This node wraps around a typical http request node in order to have a specific type indicating a call to an Openwhisk action.\n\nThis annotation is needed for later stages of the PHYSICS platform, in which calls to OW actions need to be distinguished from calls to any other external REST API. Through this information, the platform and infrastructure services can understand which parts of the flow target OW actions in case of splitting of the flow in multiple OW locations.\n\n\nFurthermore, it is used to instantiate dynamic workflows in which the developer does not know beforehand at which OW endpoint each action is going to be deployed. For this reason the relevant information for all actions is retrieved through environment variables inside the OW action container that is intended to run this orchestrator flow. The convention is as follows:\n$ACTIONNAME_OW_ENDPOINT=http://10.100.59.183:3233\n$ACTIONNAME_OW_CREDS=user:pwd\n\nIn case of a POST method, msg.payload is assumed to carry the body of the call. The node returns JSON object, since this is the typical return object of an Openwhisk action.\n\nThis flow should be executed as a service or as a OW action. In case of usage in a manual way in a typical node-red flow, without environment variable population, the information from the UI may be used.\n\nWe have maintained the UI configuration of the OW action node, so that we can use this flow also in\na typical nodered environment. However the retrieved information from the environment variables prevail\nover the set ones in the UI.\n\nPENDING: check how to pass token from UI to http request node",
"category": "PHYSICS Annotators",
"in": [
{
"x": 80,
"y": 180,
"wires": [
{
"id": "60d351e91c4f1f19"
}
]
}
],
"out": [
{
"x": 980,
"y": 280,
"wires": [
{
"id": "b84a3ec8968b0617",
"port": 0
}
]
}
],
"env": [
{
"name": "dynamicActionName",
"type": "str",
"value": "dockeraction"
}
],
"meta": {},
"color": "#C0DEED",
"icon": "node-red/function.svg"
},
{
"id": "b9c86339cffd54ea",
"type": "http request",
"z": "3352c7dddc9aad6d",
"name": "",
"method": "POST",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "a80b2fa1299e3bb8",
"persist": false,
"proxy": "",
"authType": "",
"x": 370,
"y": 180,
"wires": [
[
"27c3b0af40ccb4e0"
]
]
},
{
"id": "60d351e91c4f1f19",
"type": "function",
"z": "3352c7dddc9aad6d",
"name": "create dynamic",
"func": "//msg.headers={};\n\n\n//get info from incoming function params based on the action name given by the user in the Dynamic Action UI\nconst prophost='__'+env.get('actionName')+'_HOST';\nconst propcreds='__'+env.get('actionName')+'_CREDS';\nconst propnamespace='__'+env.get('actionName')+'_NAMESPACE';\n\n//retrieve from incoming msg params\nmsg.namespace=msg.payload.value[propnamespace];\nmsg.creds=msg.payload.value[propcreds];\nmsg.baseurl=msg.payload.value[prophost];\nmsg.url=msg.payload.value[prophost]+'namespaces/'+msg.namespace+'/actions/'+env.get('actionName');\n\n//delete info so that it does not leak into the newly called function, we dont know how the payload \n//is going to be used after that\n\ndelete msg.payload.value[propcreds];\ndelete msg.payload.value[propnamespace];\ndelete msg.payload.value[prophost]\n\n\n//denote msg.payload.value which is the normal payload to msg.payload since it is like this that the \n//inner function awaits it. Otherwise it will be in msg.payload.value.value from the follow-up OW call\nvar input=msg.payload.value;\nmsg.payload={};\nmsg.payload=input;\n\nmsg.headers={};\nvar auth = 'Basic ' + new Buffer(msg.creds).toString('base64');\nmsg.headers = {\n \"Authorization\": auth\n}\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 200,
"y": 180,
"wires": [
[
"b9c86339cffd54ea"
]
]
},
{
"id": "27c3b0af40ccb4e0",
"type": "function",
"z": "3352c7dddc9aad6d",
"name": "add activation id",
"func": "\nmsg.activationID=msg.headers['x-openwhisk-activation-id'];\n//msg.oldheaders=msg.headers;\n//msg.headers={};\nmsg.url=msg.baseurl+'namespaces/'+msg.namespace+'/activations/'+msg.activationID;\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 540,
"y": 180,
"wires": [
[
"5e15f34e1caa9fe7"
]
]
},
{
"id": "5e15f34e1caa9fe7",
"type": "subflow:789351ac.02c9d",
"z": "3352c7dddc9aad6d",
"name": "",
"env": [
{
"name": "maxAttempts",
"value": "20",
"type": "str"
},
{
"name": "pollPeriod",
"value": "3000",
"type": "num"
},
{
"name": "method",
"value": "GET",
"type": "str"
},
{
"name": "functionChain",
"value": "false",
"type": "bool"
}
],
"x": 610,
"y": 260,
"wires": [
[
"b84a3ec8968b0617",
"47a494805843dcd7"
],
[],
[]
]
},
{
"id": "b84a3ec8968b0617",
"type": "function",
"z": "3352c7dddc9aad6d",
"name": "reduce output",
"func": "//msg.headers={};\nvar response=msg.payload.response;\nmsg.payload={};\nmsg.payload.response=response;\n\nreturn msg;\n//return msg.oldmsg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 840,
"y": 280,
"wires": [
[]
]
},
{
"id": "47a494805843dcd7",
"type": "debug",
"z": "3352c7dddc9aad6d",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 610,
"y": 60,
"wires": []
},
{
"id": "a80b2fa1299e3bb8",
"type": "tls-config",
"z": "3352c7dddc9aad6d",
"name": "",
"cert": "",
"key": "",
"ca": "",
"certname": "",
"keyname": "",
"caname": "",
"servername": "",
"verifyservercert": false,
"alpnprotocol": ""
},
{
"id": "ad0beb0a878c622a",
"type": "subflow:3352c7dddc9aad6d",
"z": "93aff353a2ec21f9",
"name": "",
"env": [
{
"name": "dynamicActionName",
"value": "HelloFunctionV2_george_9d780eb8-7441-4377-a648-4a1fceee3518.json",
"type": "str"
},
{
"name": "actionName",
"value": "HelloFunctionV2_george_9d780eb8-7441-4377-a648-4a1fceee3518.json",
"type": "str"
},
{
"name": "token",
"type": "cred"
}
],
"x": 480,
"y": 260,
"wires": [
[
"d759376bc9171438"
]
]
},
{
"id": "bf09047153273cf2",
"type": "http in",
"z": "93aff353a2ec21f9",
"name": "",
"url": "/init",
"method": "post",
"upload": false,
"swaggerDoc": "",
"x": 240,
"y": 160,
"wires": [
[
"45437512996b2c7a"
]
]
},
{
"id": "45437512996b2c7a",
"type": "http response",
"z": "93aff353a2ec21f9",
"name": "",
"x": 670,
"y": 160,
"wires": []
},
{
"id": "d759376bc9171438",
"type": "http response",
"z": "93aff353a2ec21f9",
"name": "",
"statusCode": "",
"headers": {},
"x": 790,
"y": 260,
"wires": []
},
{
"id": "311e4be150c63a56",
"type": "http in",
"z": "93aff353a2ec21f9",
"name": "",
"url": "/run",
"method": "post",
"upload": false,
"swaggerDoc": "",
"x": 240,
"y": 260,
"wires": [
[
"ad0beb0a878c622a"
]
]
},
{
"id": "73608b4ce219374a",
"type": "http request",
"z": "93aff353a2ec21f9",
"name": "",
"method": "POST",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://localhost:1880/run",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"x": 630,
"y": 600,
"wires": [
[
"8c2df5bff072d0f7"
]
]
},
{
"id": "8c2df5bff072d0f7",
"type": "debug",
"z": "93aff353a2ec21f9",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 850,
"y": 600,
"wires": []
},
{
"id": "ec2cf33c7da792cd",
"type": "inject",
"z": "93aff353a2ec21f9",
"name": "TEST LOCALLY",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "{ \"value\": { \"__HelloFunctionV2_george_9d780eb8-7441-4377-a648-4a1fceee3518.json_HOST\": \"<ow-endpoint>/api/v1/\", \"__HelloFunctionV2_george_9d780eb8-7441-4377-a648-4a1fceee3518.json_NAMESPACE\": \"guest\", \"__HelloFunctionV2_george_9d780eb8-7441-4377-a648-4a1fceee3518.json_CREDS\": \"user:pwd\", \"name\": \"george\"//argument for the function }}",
"payloadType": "json",
"x": 320,
"y": 600,
"wires": [
[
"73608b4ce219374a"
]
]
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment