From 6ae79cc87d6a90b57f833cd052e8219820e21726 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 7 Dec 2020 16:05:30 +0100 Subject: [PATCH 001/185] #133: Added first part of example and body parser --- frontend/src/views/AngularWorkflow.jsx | 76 +++++++++++- frontend/src/views/AppCreator.jsx | 156 ++++++++++++++++++++++--- 2 files changed, 215 insertions(+), 17 deletions(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index e47c7c6c..9dddd448 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -845,7 +845,7 @@ const AngularWorkflow = (props) => { return response.json() }) - .then((responseJson) => { + .then((responseJson) => { // FIXME - handle versions on left bar //handleAppVersioning(responseJson) //var tmpapps = [] @@ -2872,8 +2872,64 @@ const AngularWorkflow = (props) => { placeholder = data.example } + var disabled = false + var rows = "5" + var openApiHelperText = "This is an OpenAPI specific field" + if (selectedApp.generated && selectedApp.activated && data.name === "body") { + const regex = /\${(\w+)}/g + const found = placeholder.match(regex) + console.log("THERE MAY BE THINGS INSIDE THE BODY, NO: ", found) + if (found === null) { + //setExtraBodyFields([]) + } else { + rows = "1" + disabled = true + openApiHelperText = "OpenAPI spec: fill the following fields." + console.log("SHOULD ADD TO selectedActionParameters!: ", found, selectedActionParameters) + var changed = false + for (var specKey in found) { + const tmpitem = found[specKey] + var skip = false + for (var innerkey in selectedActionParameters) { + if (selectedActionParameters[innerkey].name === tmpitem) { + skip = true + break + } + } + + if (skip) { + console.log("SKIPPING ", tmpitem) + continue + } + + changed = true + selectedActionParameters.push({ + action_field: "", + configuration: false, + description: "Generated by OpenAPI body example", + example: "", + id: "", + multiline: false, + name: tmpitem, + options: null, + required: false, + schema: {type: "string"}, + skip_multicheck: false, + tags: null, + value: "", + variant: "STATIC_VALUE", + }) + } + + if (changed) { + setSelectedActionParameters(selectedActionParameters) + } + } + } + var datafield = { }} fullWidth multiline={multiline} - rows="5" + rows={rows} color="primary" defaultValue={data.value} type={placeholder.includes("***") ? "password" : "text"} @@ -2909,6 +2965,13 @@ const AngularWorkflow = (props) => { onChange={(event) => { changeActionParameter(event, count) }} + helperText={selectedApp.generated && selectedApp.activated && data.name === "body" ? + + {openApiHelperText} + + : + null + } onBlur={(event) => { // Super basic check //if (event.target.value.startsWith("{")) { @@ -3303,7 +3366,7 @@ const AngularWorkflow = (props) => {
}
- {data.name} + {data.name.charAt(0).toUpperCase()+data.name.substring(1)}
{selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 ? null : @@ -3685,7 +3748,7 @@ const AngularWorkflow = (props) => { {selectedAction.description}
: null}
- +
@@ -6253,6 +6316,11 @@ const AngularWorkflow = (props) => { getAppAuthentication() setUpdate(authenticationOption.id) + /* + {selectedAction.authentication.map(data => ( + + */ + } return ( diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index f47330c9..210a433d 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -4,6 +4,7 @@ import {BrowserView, MobileView} from "react-device-detect"; import {Link} from 'react-router-dom'; import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Button from '@material-ui/core/Button'; import Divider from '@material-ui/core/Divider'; @@ -218,6 +219,7 @@ const AppCreator = (props) => { const [actions, setActions] = useState([]) const [errorCode, setErrorCode] = useState("") const [appBuilding, setAppBuilding] = useState(false) + const [extraBodyFields, setExtraBodyFields] = useState([]) //const [actions, setActions] = useState([{ // "name": "Get workflows", @@ -253,6 +255,7 @@ const AppCreator = (props) => { "queries": [], "body": "", "errors": [], + "example_response": "", "method": actionNonBodyRequest[0], }); @@ -262,7 +265,7 @@ const AppCreator = (props) => { if (firstrequest) { setFirstrequest(false) if (window.location.pathname.includes("apps/edit")) { - setIsEditing(true) + setIsEditing(true) handleEditApp() } else { checkQuery() @@ -433,6 +436,22 @@ const AppCreator = (props) => { "paths": [], "body": "", "errors": [], + "example_response": "", + } + + // HAHAHA wtf is this. + if (methodvalue.responses !== undefined) { + if (methodvalue.responses.default !== undefined) { + if (methodvalue.responses.default.content !== undefined) { + if (methodvalue.responses.default.content["text/plain"] !== undefined) { + if (methodvalue.responses.default.content["text/plain"]["schema"] !== undefined) { + if (methodvalue.responses.default.content["text/plain"]["schema"]["example"] !== undefined) { + newaction.example_response = methodvalue.responses.default.content["text/plain"]["schema"]["example"] + } + } + } + } + } } for (var key in methodvalue.parameters) { @@ -469,6 +488,7 @@ const AppCreator = (props) => { } } + if (newaction.name === "" || newaction.name === undefined) { // Find a unique part of the string // FIXME: Looks for length between /, find the one where they differ @@ -668,7 +688,14 @@ const AppCreator = (props) => { "responses": { "default": { "description": "default", - "schema": {} + "content": { + "text/plain": { + "schema": { + "type": "string", + "example": "", + }, + }, + }, } }, "summary": item.name, @@ -679,6 +706,35 @@ const AppCreator = (props) => { //console.log("ACTION: ", item) + if (item.example_response !== undefined && item.example_response.length > 0) { + // FIXME: Shallow copy of the string + var showResult = Object.assign("", item.example_response).trim() + showResult = showResult.split(" None").join(" \"None\"") + showResult = showResult.split("\'").join("\"") + showResult = showResult.split(" False").join(" false") + showResult = showResult.split(" True").join(" true") + + var jsonvalid = true + try { + const tmp = String(JSON.parse(showResult)) + if (!showResult.includes("{") && !showResult.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } + + data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.type = "string" + if (jsonvalid) { + // FIXME: Add a JSON parser here - don't run it as a string. + data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.example = showResult + + } else { + data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.example = item.example_response + + } + } + if (item.queries.length > 0) { for (var querykey in item.queries) { const queryitem = item.queries[querykey] @@ -761,7 +817,7 @@ const AppCreator = (props) => { "type": "string", }, } - + // FIXME - add application/json if JSON example? data.paths[item.url][item.method.toLowerCase()]["requestBody"] = { "description": "Generated by Shuffler.io", @@ -773,6 +829,18 @@ const AppCreator = (props) => { }, } + /* + data.paths[item.url][item.method.toLowerCase()]["requestBody"] = { + "description": "Generated by Shuffler.io", + "required": required, + "content": { + "example": { + "example": item.body, + }, + }, + } + */ + data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) } @@ -782,7 +850,6 @@ const AppCreator = (props) => { const headersSplit = item.headers.split("\n") for (var key in headersSplit) { const header = headersSplit[key] - console.log("HEADER: ", header) var key = "" var value = "" if (header.length > 0 && header.includes("= ")) { @@ -1137,12 +1204,15 @@ const AppCreator = (props) => { setUrlPathQueries(data.queries) setUrlPath(data.url) setActionsModalOpen(true) + + if (data["body"] !== undefined && data["body"] !== null && data["body"].length > 0) { + findBodyParams(data["body"]) + } }}>
{url} - {data.name} @@ -1177,24 +1247,52 @@ const AppCreator = (props) => { const setActionField = (field, value) => { currentAction[field] = value setCurrentAction(currentAction) + //setUrlPathQueries(currentAction.queries) } + const findBodyParams = (body) => { + const regex = /\${(\w+)}/g + const found = body.match(regex) + console.log("FOUND: ", found) + if (found === null) { + setExtraBodyFields([]) + } else { + setExtraBodyFields(found) + } + } + const bodyInfo = actionBodyRequest.includes(currentActionMethod) ? -
- Body - used as example in action argument +
+ Request Body: {extraBodyFields.length > 0 ? + + Variables: {extraBodyFields.join(", ")} + + : + + {`Add variables with \$\{ variable_name }`} + + } setActionField("body", e.target.value)} + onChange={e => { + setActionField("body", e.target.value) + findBodyParams(e.target.value) + }} key={currentAction} + helperText={ + + Shows an example body to the user. ${} creates variables. + + } InputProps={{ classes: { notchedOutline: classes.notchedOutline, @@ -1205,9 +1303,40 @@ const AppCreator = (props) => { }} /> +
+
: null + const exampleResponse = +
+ Example success response + setActionField("example_response", e.target.value)} + helperText={ + Helps with autocompletion and understanding of the endpoint + } + key={currentAction} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> +
+ const addActionToView = (errors) => { currentAction.errors = errors currentAction.queries = urlPathQueries @@ -1361,7 +1490,6 @@ const AppCreator = (props) => { open={actionsModalOpen} fullWidth onClose={() => { - console.log("CLOSED?") setUrlPath("") setCurrentAction({ "name": "", @@ -1564,7 +1692,7 @@ const AppCreator = (props) => { addPathQuery() }}>New query
- Headers - static for the action + Headers: static for the action { id="standard-required" defaultValue={currentAction["headers"]} multiline - rows="5" + rows="2" onChange={e => setActionField("headers", e.target.value)} helperText={Headers that are part of the request. Default: EMPTY} InputProps={{ @@ -1588,6 +1716,8 @@ const AppCreator = (props) => { }} /> {bodyInfo} + + {exampleResponse} @@ -188,9 +205,151 @@ const OrgHeader = (props) => {
{orgSaveButton}
-
-
+
+ +
+ { + setExpanded(!expanded) + }}> + {expanded ? + + : + + } + + {expanded ? + + + + + App Download URL + + { + setAppDownloadUrl(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> + + + + + + App Download Branch + + { + setAppDownloadBranch(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> + + + + + + Workflow Download URL + + { + setWorkflowDownloadUrl(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> + + + + + + Workflow Download Branch + + { + setWorkflowDownloadBranch(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> + + + {/* + + {expanded ? + + : + + } + + */} + + : + null + } +
) } diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 30f759d9..8eeb54d6 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -134,6 +134,7 @@ const Apps = (props) => { const [field2, setField2] = React.useState("") const [cursearch, setCursearch] = React.useState("") const [sharingConfiguration, setSharingConfiguration] = React.useState("you") + const [downloadBranch, setDownloadBranch] = React.useState("master") const [isDropzone, setIsDropzone] = React.useState(false); const upload = React.useRef(null); @@ -959,6 +960,7 @@ const Apps = (props) => { const parsedData = { "url": url, + "branch": downloadBranch || 'master' } if (field1.length > 0) { @@ -988,18 +990,23 @@ const Apps = (props) => { } setIsLoading(false) stop() + setValidation(false) return response.json() }) .then((responseJson) => { - console.log("DATA: ", responseJson) - if (responseJson.reason !== undefined) { - alert.error("Failed loading: "+responseJson.reason) - } + console.log("DATA: ", responseJson) + if (responseJson.reason !== undefined) { + alert.error("Failed loading: "+responseJson.reason) + } }) .catch(error => { console.log("ERROR: ", error.toString()) alert.error(error.toString()) + + stop() + setIsLoading(false) + setValidation(false) }) } @@ -1321,6 +1328,25 @@ const Apps = (props) => { placeholder="https://github.com/frikky/shuffle-apps" fullWidth /> + Branch (default value is "master"): +
+ setDownloadBranch(e.target.value)} + placeholder="master" + fullWidth + /> +
Authentication (optional - private repos etc):
From 82d42cf78225e1e3d5c4f2e5ebcddfb993697303 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 13 Dec 2020 12:43:17 +0100 Subject: [PATCH 015/185] #170: More stable multi-loop structure --- backend/app_sdk/app_base.py | 144 ++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 70 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 2e1afe7d..eeb0fbbf 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -170,18 +170,12 @@ class AppBase: print("PARAMLIST: %s" % paramlist) - #newlist[subitem[0]] - #if len(newlist) > 0: # itemlength = len(newlist[0]) # How do we get it back, ordered? #for item in cartesian: - - - - #print("Listlengths: %s" % listlengths) #paramlist = [baseparams] @@ -1563,15 +1557,27 @@ class AppBase: print("APP_SDK DONE: Starting MULTI execution (length: %d) with values %s" % (minlength, multi_parameters)) # 1. Use number of executions based on the arrays being similar # 2. Find the right value from the parsed multi_params - results = [] - json_object = False - for i in range(0, minlength): - # To be able to use the results as a list: - print("1: %s" % multi_parameters) - #baseparams = json.loads(json.dumps(multi_parameters)) - baseparams = copy.deepcopy(multi_parameters) - print("2: %s: %s" % (type(baseparams), baseparams)) + print("Running without outer loop") + json_object = False + results = await self.run_recursed_items(func, multi_parameters, {}) + if isinstance(results, dict) or isinstance(results, list): + json_object = True + + #for i in range(0, minlength): + # # To be able to use the results as a list: + # print("1: %s" % multi_parameters) + # #baseparams = json.loads(json.dumps(multi_parameters)) + # baseparams = copy.deepcopy(multi_parameters) + + # print("2: %s: %s" % (type(baseparams), baseparams)) + + # print("4") + # print("Running with params (1): %s" % baseparams) + + # results = await self.run_recursed_items(func, baseparams, {}) + # if isinstance(results, dict) or isinstance(results, list): + # json_object = True # {'call': ['GoogleSafebrowsing_2_0', 'VirusTotal_GetReport_3_0']} # 1. Check if list length is same as minlength @@ -1580,68 +1586,66 @@ class AppBase: # arraylength = 4 ["1", "2", "3", "4"] # minlength = 12 - 12/3 = 4 per item = ["1", "1", "1", "1", "2", "2", ...] - try: - firstlist = True - for key, value in baseparams.items(): - print("Itemtype: %s" % type(value)) - if isinstance(value, list): - try: - newvalue = value[i] - except IndexError: - pass + #try: + # firstlist = True + # for key, value in baseparams.items(): + # print("Itemtype: %s" % type(value)) + # if isinstance(value, list): + # try: + # newvalue = value[i] + # except IndexError: + # pass - if len(value) != minlength and len(value) > 0: - newarray = [] - print("VALUE: ", value) - additiontime = minlength/len(value) - print("Bad length for value: %d - should be %d. Additiontime: %d" % (len(value), minlength, additiontime)) - if firstlist: - print("Running normal list (FIRST)") - for subvalue in value: - for number in range(int(additiontime)): - newarray.append(subvalue) - else: - #print("Running secondary lists") - ## 1. Set up length of array - ## 2. Put values spread out - # FIXME: This works well, except if lists are same length - newarray = [""] * minlength + # if len(value) != minlength and len(value) > 0: + # newarray = [] + # print("VALUE: ", value) + # additiontime = minlength/len(value) + # print("Bad length for value: %d - should be %d. Additiontime: %d" % (len(value), minlength, additiontime)) + # if firstlist: + # print("Running normal list (FIRST)") + # for subvalue in value: + # for number in range(int(additiontime)): + # newarray.append(subvalue) + # else: + # #print("Running secondary lists") + # ## 1. Set up length of array + # ## 2. Put values spread out + # # FIXME: This works well, except if lists are same length + # newarray = [""] * minlength - cnt = 0 - for number in range(int(additiontime)): - for subvaluerange in range(len(value)): - # newlocation = number+(additiontime*subvaluerange) - # print("%d+(%d*%d) = %d. VAL: %s" % (number, additiontime, subvaluerange, newlocation, value[subvaluerange])) - # Reverse if same length? - if int(minlength/len(value)) == len(value): - tmp = int(len(value)-subvaluerange-1) - print("NEW: %d" % tmp) - newarray[cnt] = value[tmp] - else: - newarray[cnt] = value[subvaluerange] - cnt += 1 + # cnt = 0 + # for number in range(int(additiontime)): + # for subvaluerange in range(len(value)): + # # newlocation = number+(additiontime*subvaluerange) + # # print("%d+(%d*%d) = %d. VAL: %s" % (number, additiontime, subvaluerange, newlocation, value[subvaluerange])) + # # Reverse if same length? + # if int(minlength/len(value)) == len(value): + # tmp = int(len(value)-subvaluerange-1) + # print("NEW: %d" % tmp) + # newarray[cnt] = value[tmp] + # else: + # newarray[cnt] = value[subvaluerange] + # cnt += 1 - #print("Newarray =", newarray) - newvalue = newarray[i] - firstlist = False + # #print("Newarray =", newarray) + # newvalue = newarray[i] + # firstlist = False - baseparams[key] = newvalue + # baseparams[key] = newvalue - print("3") - except IndexError as e: - print("IndexError: %s" % e) - baseparams[key] = "IndexError: %s" % e - except KeyError as e: - print("KeyError: %s" % e) - baseparams[key] = "KeyError: %s" % e + # print("3") + #except IndexError as e: + # print("IndexError: %s" % e) + # baseparams[key] = "IndexError: %s" % e + #except KeyError as e: + # print("KeyError: %s" % e) + # baseparams[key] = "KeyError: %s" % e + #print("4") + #print("Running with params (1): %s" % baseparams) - - print("4") - print("Running with params (1): %s" % baseparams) - - results = await self.run_recursed_items(func, baseparams, {}) - if isinstance(results, dict) or isinstance(results, list): - json_object = True + #results = await self.run_recursed_items(func, baseparams, {}) + #if isinstance(results, dict) or isinstance(results, list): + # json_object = True # Check the structure here. If "isloop", try to recurse? # ret, is_loop = recurse_json(innervalue, parsersplit[outercnt+1:]) From a5b818772f81dd05b29597282d8d1e49fb6a3dab Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 13 Dec 2020 12:43:48 +0100 Subject: [PATCH 016/185] #216: Added backend components for Org settings --- backend/app_sdk/build.sh | 2 +- backend/go-app/main.go | 25 ++++++++++++++----------- backend/go-app/walkoff.go | 17 ++++++++++++++++- docker-compose.yml | 2 +- frontend/src/views/AngularWorkflow.jsx | 6 +++--- functions/onprem/orborus/build.sh | 2 +- 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 2260c6dc..9d39852b 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.3 +VERSION=0.8.31 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/main.go b/backend/go-app/main.go index e855d058..d68218e9 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3435,18 +3435,18 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { return } - parsedBody := string(body) + //parsedBody := string(body) //parsedBody = strings.Replace(parsedBody, "\"", "\\\"", -1) - if len(parsedBody) > 0 { - if string(parsedBody[0]) == `"` && string(parsedBody[len(parsedBody)-1]) == "\"" { - parsedBody = parsedBody[1 : len(parsedBody)-1] - } - } + //if len(parsedBody) > 0 { + // if string(parsedBody[0]) == `"` && string(parsedBody[len(parsedBody)-1]) == "\"" { + // parsedBody = parsedBody[1 : len(parsedBody)-1] + // } + //} newBody := ExecutionStruct{ Start: hook.Start, ExecutionSource: "webhook", - ExecutionArgument: string(parsedBody), + ExecutionArgument: string(body), } b, err := json.Marshal(newBody) @@ -7611,10 +7611,11 @@ func handleEditOrg(resp http.ResponseWriter, request *http.Request) { } type ReturnData struct { - Image string `json:"image" datastore:"image"` - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description"` - OrgId string `json:"org_id" datastore:"org_id"` + Image string `json:"image" datastore:"image"` + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + OrgId string `json:"org_id" datastore:"org_id"` + Defaults Defaults `json:"defaults" datastore:"defaults"` } var tmpData ReturnData @@ -7685,6 +7686,8 @@ func handleEditOrg(resp http.ResponseWriter, request *http.Request) { org.Image = tmpData.Image org.Name = tmpData.Name org.Description = tmpData.Description + org.Defaults = tmpData.Defaults + //log.Printf("Org: %#v", org) err = setOrg(ctx, *org, org.Id) if err != nil { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 95346ae6..d35d36f6 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -116,6 +116,14 @@ type Org struct { SyncFeatures SyncFeatures `json:"sync_features" datastore:"sync_features"` Created int64 `json:"created" datastore:"created"` Edited int64 `json:"edited" datastore:"edited"` + Defaults Defaults `json:"defaults" datastore:"defaults"` +} + +type Defaults struct { + AppDownloadRepo string `json:"app_download_repo" datastore:"app_download_repo"` + AppDownloadBranch string `json:"app_download_branch" datastore:"app_download_branch"` + WorkflowDownloadRepo string `json:"workflow_download_repo" datastore:"workflow_download_repo"` + WorkflowDownloadBranch string `json:"workflow_download_branch" datastore:"workflow_download_branch"` } type AppAuthenticationStorage struct { @@ -5134,9 +5142,12 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) { return } - // Field1 & 2 can be a lot of things.. + // Field1 & 2 can be a lot of things. + // Field1 = Username + // Field2 = Password type tmpStruct struct { URL string `json:"url"` + Branch string `json:"branch"` Field1 string `json:"field_1"` Field2 string `json:"field_2"` ForceUpdate bool `json:"force_update"` @@ -5159,6 +5170,10 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) { URL: tmpBody.URL, } + if len(tmpBody.Branch) > 0 && tmpBody.Branch != "master" && tmpBody.Branch != "main" { + cloneOptions.ReferenceName = plumbing.ReferenceName(tmpBody.Branch) + } + // FIXME: Better auth. if len(tmpBody.Field1) > 0 && len(tmpBody.Field2) > 0 { cloneOptions.Auth = &http2.BasicAuth{ diff --git a/docker-compose.yml b/docker-compose.yml index 5ed77582..f5f69046 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,7 +45,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.0 + image: ghcr.io/frikky/shuffle-orborus:0.8.3 container_name: shuffle-orborus hostname: shuffle-orborus networks: diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index ba891857..c4b7ad08 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -402,7 +402,7 @@ const AngularWorkflow = (props) => { if (!visited.includes(item.action.label)) { if (executionRunning) { - alert.show("WAITING FOR "+item.action.label+" with result "+item.result) + //alert.show("WAITING FOR "+item.action.label+" with result "+item.result) visited.push(item.action.label) setVisited(visited) } @@ -424,7 +424,7 @@ const AngularWorkflow = (props) => { if (!visited.includes(item.action.label)) { if (executionRunning) { - alert.show("Success in node "+item.action.label) + //alert.show("Success in node "+item.action.label) //+" with result "+item.result) visited.push(item.action.label) setVisited(visited) @@ -459,7 +459,7 @@ const AngularWorkflow = (props) => { currentnode.addClass('failure-highlight') if (!visited.includes(item.action.label)) { - alert.error("Success for "+item.action.label+" with result "+item.result) + alert.error("Error for "+item.action.label+" with result "+item.result) visited.push(item.action.label) setVisited(visited) } diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 08ddda2f..010efe8d 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -6,6 +6,6 @@ echo "Running docker build with $NAME:$VERSION" docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION #docker push frikky/$NAME:$VERSION -#docker push frikky/shuffle:$NAME # docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION +docker push frikky/shuffle:$NAME docker push ghcr.io/frikky/$NAME:$VERSION From cd5bd6a348476ea54d97f279824682ee0c2e5032 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 14 Dec 2020 10:36:03 +0100 Subject: [PATCH 017/185] Minor changes --- backend/app_sdk/app_base.py | 22 +++++++++++++++++++--- backend/go-app/codegen.go | 7 +++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index eeb0fbbf..e8bd3ca9 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -218,10 +218,25 @@ class AppBase: param_multiplier = await self.get_param_multipliers(newparams) print("Multiplier length: %d" % len(param_multiplier)) for subparams in param_multiplier: - ret.append(await func(**subparams)) + tmp = await func(**subparams) + + print("Return from execution: %s" % ret) + if tmp == None: + ret.append("") + elif isinstance(tmp, dict): + ret.append(tmp) + elif isinstance(tmp, list): + ret.append(tmp) + else: + tmp = tmp.replace("\"", "\\\"", -1) + + try: + ret.append(json.loads(tmp)) + except json.decoder.JSONDecodeError as e: + #print("Json: %s" % e) + ret.append(tmp) print("Ret length: %d" % len(ret)) - if len(ret) == 1: ret = ret[0] @@ -1477,6 +1492,7 @@ class AppBase: print("SCHEMA ERROR IN FILE HANDLING: %s" % e) # Fix lists here + # FIXME: This doesn't really do anything anymore print("CHECKING multi execution list!") if len(multi_execution_lists) > 0: print("\n Multi execution list has more data: %d" % len(multi_execution_lists)) @@ -1558,7 +1574,7 @@ class AppBase: # 1. Use number of executions based on the arrays being similar # 2. Find the right value from the parsed multi_params - print("Running without outer loop") + print("Running WITHOUT outer loop") json_object = False results = await self.run_recursed_items(func, multi_parameters, {}) if isinstance(results, dict) or isinstance(results, list): diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 2099affb..fe2b6967 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -948,6 +948,12 @@ func validateParameterName(name string) string { } } + newname = strings.ReplaceAll(newname, " ", "_") + newname = strings.ReplaceAll(newname, ",", "_") + newname = strings.ReplaceAll(newname, ".", "_") + newname = strings.ReplaceAll(newname, "|", "_") + newname = strings.ReplaceAll(newname, "-", "_") + return newname } @@ -1001,6 +1007,7 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ parsedName = strings.ReplaceAll(parsedName, ",", "_") parsedName = strings.ReplaceAll(parsedName, ".", "_") parsedName = strings.ReplaceAll(parsedName, "|", "_") + parsedName = strings.ReplaceAll(parsedName, "-", "_") parsedName = validateParameterName(parsedName) param.Value.Name = parsedName path.Connect.Parameters[counter].Value.Name = parsedName From 11c41f7e70b003f58028b9be234d2a11baaa5252 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 14 Dec 2020 18:02:55 +0100 Subject: [PATCH 018/185] #170: Fixed looping recursion issue with single-item list --- backend/app_sdk/app_base.py | 23 ++++++++++++++++++++--- backend/app_sdk/build.sh | 2 +- backend/go-app/walkoff.go | 4 ++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index e8bd3ca9..1d5bb7ee 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -127,7 +127,10 @@ class AppBase: print("%s is not a list: " % value) print("Listlengths: %s" % listlengths) - if len(listlengths) <= 1: + if len(listlengths) == 0: + print("NO multiplier. Running a single iteration.") + paramlist.append(baseparams) + elif len(listlengths) == 1: print("NO MULTIPLIER NECESSARY. Length is %d" % len(listitems)) for item in listitems: # This loops should always be length 1 @@ -189,6 +192,17 @@ class AppBase: newparams = {} for key, value in baseparams.items(): + if isinstance(value, list) and len(value) > 0: + print("In list check") + try: + value[0] = json.loads(value[0]) + except json.decoder.JSONDecodeError as e: + print("JSON casting error: %s" % e) + except TypeError as e: + print("TypeError: %s" % e) + + print("POST list check") + if isinstance(value, list) and len(value) == 1 and isinstance(value[0], list): try: loop_wrapper[key] += 1 @@ -202,6 +216,7 @@ class AppBase: has_loop = True else: print("Key %s is NOT a list within a list: %s" % (key, value)) + newparams[key] = value results = [] @@ -216,6 +231,7 @@ class AppBase: # If here: check for multipliers within this scope. ret = [] param_multiplier = await self.get_param_multipliers(newparams) + print("Multiplier length: %d" % len(param_multiplier)) for subparams in param_multiplier: tmp = await func(**subparams) @@ -248,7 +264,7 @@ class AppBase: results.append(ret) json_object = True elif isinstance(ret, list): - results.append(ret) + results = ret json_object = True else: ret = ret.replace("\"", "\\\"", -1) @@ -1324,7 +1340,7 @@ class AppBase: print("Before first part in multiexec!") handled = False if len(actualitem[0]) > 2 and actualitem[0][1] == "SHUFFLE_NO_SPLITTER": - print("Pre replacement: %s" % actualitem[0][2]) + print("(1) Pre replacement: %s" % actualitem[0][2]) tmpitem = value replacement = actualitem[0][2] @@ -1379,6 +1395,7 @@ class AppBase: multi_execution_lists.append(json_replacement) print("MULTI finished: %s" % json_replacement) else: + print("(2) Pre replacement: %s" % actualitem) # This is here to handle for loops within variables.. kindof # 1. Find the length of the longest array # 2. Build an array with the base values based on parameter["value"] diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 9d39852b..fe988a1c 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.31 +VERSION=0.8.32 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index d35d36f6..0437b7a0 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -5910,7 +5910,7 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally (get executions): %s", err) + log.Printf("Failed getting the workflow %s locally (get executions): %s", fileId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -5987,7 +5987,7 @@ func getAllWorkflowApps(ctx context.Context) ([]WorkflowApp, error) { _, err := dbclient.GetAll(ctx, q, &allworkflowapps) if err != nil { if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { - q := datastore.NewQuery("workflowapp").Limit(30).Order("-edited") + q := datastore.NewQuery("workflowapp").Limit(40).Order("-edited") _, err := dbclient.GetAll(ctx, q, &allworkflowapps) if err != nil { return []WorkflowApp{}, err From 50bcf3bf3da58507dad4bcf7d66850f4be4df752 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 15 Dec 2020 16:12:53 +0100 Subject: [PATCH 019/185] #170: Added parameter looping check for special apps --- backend/app_sdk/app_base.py | 34 ++++++++++++++++++++++++++-------- frontend/src/views/Apps.jsx | 4 ++-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 1d5bb7ee..fd78155f 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -105,21 +105,36 @@ class AppBase: listlengths = [] all_lists = [] all_list_keys = [] + + #check_value = "$Filter_list_testing.wrapper.#.tmp" + #self.action = action + + for key, value in baseparams.items(): + check_value = "" + for param in self.action["parameters"]: + if param["name"] == key: + check_value = param["value"] + break + + print("\nCHECK: %s" % check_value) if isinstance(value, list): if len(value) <= 1: if len(value) == 1: baseparams[key] = value[0] else: - if len(value) not in listlengths: - listlengths.append(len(value)) + if not check_value.endswith("#"): + print("Adding WITHOUT looping list") + else: + if len(value) not in listlengths: + listlengths.append(len(value)) - listitems.append( - { - key: len(value) - } - ) + listitems.append( + { + key: len(value) + } + ) all_list_keys.append(key) all_lists.append(baseparams[key]) @@ -132,6 +147,7 @@ class AppBase: paramlist.append(baseparams) elif len(listlengths) == 1: print("NO MULTIPLIER NECESSARY. Length is %d" % len(listitems)) + for item in listitems: # This loops should always be length 1 for key, value in item.items(): @@ -244,7 +260,7 @@ class AppBase: elif isinstance(tmp, list): ret.append(tmp) else: - tmp = tmp.replace("\"", "\\\"", -1) + #tmp = tmp.replace("\"", "\\\"", -1) try: ret.append(json.loads(tmp)) @@ -432,6 +448,8 @@ class AppBase: "started_at": int(time.time()), "status": "EXECUTING" } + + self.action = action self.logger.info("ACTION RESULT (start): %s", action_result) if len(self.action) == 0: diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 8eeb54d6..3113bc31 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -589,10 +589,10 @@ const Apps = (props) => {
{imageline}
-
+

{newAppname}

Version {selectedApp.app_version}

-

{description}

+

{description}

{activateButton} From be9fef6a851e40dbeecee729b0202c4a65edcf89 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 18 Dec 2020 03:44:35 +0100 Subject: [PATCH 020/185] #204: Added file listing and downloads for org --- backend/go-app/files.go | 82 ++++++++++- backend/go-app/main.go | 14 +- backend/go-app/walkoff.go | 31 ++-- frontend/package.json | 2 +- frontend/src/views/Admin.jsx | 277 +++++++++++++++++++++++++++++------ 5 files changed, 340 insertions(+), 66 deletions(-) diff --git a/backend/go-app/files.go b/backend/go-app/files.go index a2856e2b..b9b30a6d 100644 --- a/backend/go-app/files.go +++ b/backend/go-app/files.go @@ -42,6 +42,7 @@ type File struct { DownloadPath string `json:"download_path" datastore:"download_path"` Md5sum string `json:"md5_sum" datastore:"md5_sum"` Sha256sum string `json:"sha256_sum" datastore:"sha256_sum"` + FileSize int64 `json:"filesize" datastore:"filesize"` } var basepath = os.Getenv("SHUFFLE_FILE_LOCATION") @@ -100,6 +101,51 @@ func fileExists(filename string) bool { return !info.IsDir() } +func handleGetFiles(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + // 1. Check user directly + // 2. Check workflow execution authorization + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("[INFO] INITIAL Api authentication failed in file LIST: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if user.Role != "admin" { + log.Printf("[AUTH] User isn't admin") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Need to be admin"}`))) + return + } + + ctx := context.Background() + files, err := getAllFiles(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("[ERROR] Failed to get files: %s", err) + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error getting files."}`))) + return + } + + log.Printf("Got %d files for org %s", len(files), user.ActiveOrg.Id) + newBody, err := json.Marshal(files) + if err != nil { + log.Printf("[ERROR] Failed marshaling files: %s", err) + resp.WriteHeader(500) + resp.Write([]byte(`{"success": false, "reason": "Failed to marshal files"}`)) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(newBody)) +} + func handleGetFileMeta(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -417,8 +463,18 @@ func handleGetFileContent(resp http.ResponseWriter, request *http.Request) { Openfile, err := os.Open(downloadPath) defer Openfile.Close() //Close after function return if err != nil { + file.Status = "deleted" + err = setFile(ctx, *file) + if err != nil { + log.Printf("Failed setting file to uploading") + resp.WriteHeader(500) + resp.Write([]byte(`{"success": false, "reason": "Failed setting file to uploading"}`)) + return + } + //File not found, send 404 - http.Error(resp, "File not found.", 404) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "File doesn't exist locally"}`)) return } @@ -552,6 +608,7 @@ func handleUploadFile(resp http.ResponseWriter, request *http.Request) { var buf bytes.Buffer io.Copy(&buf, parsedFile) contents := buf.Bytes() + file.FileSize = int64(len(contents)) md5 := md5sum(contents) buf.Reset() @@ -754,6 +811,9 @@ func getFile(ctx context.Context, id string) (*File, error) { func setFile(ctx context.Context, file File) error { // clear session_token and API_token for user + timeNow := time.Now().Unix() + file.UpdatedAt = timeNow + k := datastore.NameKey("Files", file.Id, nil) if _, err := dbclient.Put(ctx, k, &file); err != nil { log.Println(err) @@ -762,3 +822,23 @@ func setFile(ctx context.Context, file File) error { return nil } + +func getAllFiles(ctx context.Context, orgId string) ([]File, error) { + var files []File + q := datastore.NewQuery("Files").Filter("org_id =", orgId).Order("-updated_at").Limit(100) + + _, err := dbclient.GetAll(ctx, q, &files) + if err != nil { + if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { + q = q.Limit(50) + _, err := dbclient.GetAll(ctx, q, &files) + if err != nil { + return []File{}, err + } + } else { + return []File{}, err + } + } + + return files, nil +} diff --git a/backend/go-app/main.go b/backend/go-app/main.go index d68218e9..7257fa6f 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -108,6 +108,7 @@ type StatisticsItem struct { Total int64 `json:"total" datastore:"total"` Fieldname string `json:"field_name" datastore:"field_name"` Data []StatisticsData `json:"data" datastore:"data"` + OrgId string `json:"org_id" datastore:"org_id"` } // "Execution by status" @@ -1148,7 +1149,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { } } - err = increaseStatisticsField(ctx, "successful_register", username, 1) + err = increaseStatisticsField(ctx, "successful_register", username, 1, org.Id) if err != nil { log.Printf("Failed to increase total apps loaded stats: %s", err) } @@ -3471,7 +3472,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // OrgId: activeOrgs[0].Id, workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest) if err == nil { - err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1) + err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) if err != nil { log.Printf("Failed to increase total apps loaded stats: %s", err) } @@ -3679,7 +3680,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { return } - err = increaseStatisticsField(ctx, "total_workflow_triggers", requestdata.Workflow, 1) + err = increaseStatisticsField(ctx, "total_workflow_triggers", requestdata.Workflow, 1, user.ActiveOrg.Id) if err != nil { log.Printf("[INFO] Failed to increase total workflows: %s", err) } @@ -6481,12 +6482,12 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { // Backup every single one setOpenApiDatastore(ctx, api.ID, parsed) - err = increaseStatisticsField(ctx, "total_apps_created", newmd5, 1) + err = increaseStatisticsField(ctx, "total_apps_created", newmd5, 1, user.ActiveOrg.Id) if err != nil { log.Printf("Failed to increase success execution stats: %s", err) } - err = increaseStatisticsField(ctx, "openapi_apps_created", newmd5, 1) + err = increaseStatisticsField(ctx, "openapi_apps_created", newmd5, 1, user.ActiveOrg.Id) if err != nil { log.Printf("Failed to increase success execution stats: %s", err) } @@ -6873,7 +6874,7 @@ func remoteOrgJobHandler(org Org, interval int) error { func runInit(ctx context.Context) { // Setting stats for backend starts (failure count as well) log.Printf("Starting INIT setup") - err := increaseStatisticsField(ctx, "backend_executions", "", 1) + err := increaseStatisticsField(ctx, "backend_executions", "", 1, "") if err != nil { log.Printf("Failed increasing local stats: %s", err) } @@ -8125,6 +8126,7 @@ func initHandlers() { r.HandleFunc("/api/v1/files/{fileId}/upload", handleUploadFile).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/files/{fileId}", handleGetFileMeta).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/files/{fileId}", handleDeleteFile).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/files", handleGetFiles).Methods("GET", "OPTIONS") http.Handle("/", r) } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 0437b7a0..2045cf82 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -440,7 +440,7 @@ type AppExecutionExample struct { // This might be... a bit off, but that's fine :) // This might also be stupid, as we want timelines and such // Anyway, these are super basic stupid stats. -func increaseStatisticsField(ctx context.Context, fieldname, id string, amount int64) error { +func increaseStatisticsField(ctx context.Context, fieldname, id string, amount int64, orgId string) error { // 1. Get current stats // 2. Increase field(s) @@ -461,6 +461,7 @@ func increaseStatisticsField(ctx context.Context, fieldname, id string, amount i if strings.Contains(fmt.Sprintf("%s", err), "entity") { statisticsItem = StatisticsItem{ Total: amount, + OrgId: orgId, Fieldname: fieldname, Data: []StatisticsData{ newData, @@ -1069,7 +1070,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } newResults = append(newResults, newResult) - increaseStatisticsField(ctx, "workflow_execution_actions_skipped", workflowExecution.Workflow.ID, 1) + increaseStatisticsField(ctx, "workflow_execution_actions_skipped", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) } } } @@ -1094,12 +1095,12 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl workflowExecution.Results = newResults if workflowExecution.Status == "ABORTED" { - err = increaseStatisticsField(ctx, "workflow_executions_aborted", workflowExecution.Workflow.ID, 1) + err = increaseStatisticsField(ctx, "workflow_executions_aborted", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) if err != nil { log.Printf("Failed to increase aborted execution stats: %s", err) } } else if workflowExecution.Status == "FAILURE" { - err = increaseStatisticsField(ctx, "workflow_executions_failure", workflowExecution.Workflow.ID, 1) + err = increaseStatisticsField(ctx, "workflow_executions_failure", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) if err != nil { log.Printf("Failed to increase failure execution stats: %s", err) } @@ -1237,7 +1238,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl workflowExecution.LastNode = actionResult.Action.ID } - err = increaseStatisticsField(ctx, "workflow_executions_success", workflowExecution.Workflow.ID, 1) + err = increaseStatisticsField(ctx, "workflow_executions_success", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) if err != nil { log.Printf("Failed to increase success execution stats: %s", err) } @@ -1525,7 +1526,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() log.Printf("Saved new workflow %s with name %s", workflow.ID, workflow.Name) - err = increaseStatisticsField(ctx, "total_workflows", workflow.ID, 1) + err = increaseStatisticsField(ctx, "total_workflows", workflow.ID, 1, workflow.OrgId) if err != nil { log.Printf("Failed to increase total workflows stats: %s", err) } @@ -1725,7 +1726,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { } } - err = increaseStatisticsField(ctx, "total_workflow_triggers", workflow.ID, -1) + err = increaseStatisticsField(ctx, "total_workflow_triggers", workflow.ID, -1, workflow.OrgId) if err != nil { log.Printf("Failed to increase total workflows: %s", err) } @@ -1741,7 +1742,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { return } - err = increaseStatisticsField(ctx, "total_workflows", fileId, -1) + err = increaseStatisticsField(ctx, "total_workflows", fileId, -1, workflow.OrgId) if err != nil { log.Printf("Failed to increase total workflows: %s", err) } @@ -2314,7 +2315,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { totalOldActions := len(tmpworkflow.Actions) totalNewActions := len(workflow.Actions) - err = increaseStatisticsField(ctx, "total_workflow_actions", workflow.ID, int64(totalNewActions-totalOldActions)) + err = increaseStatisticsField(ctx, "total_workflow_actions", workflow.ID, int64(totalNewActions-totalOldActions), workflow.OrgId) if err != nil { log.Printf("Failed to change total actions data: %s", err) } @@ -2486,7 +2487,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { return } - err = increaseStatisticsField(ctx, "workflow_executions_aborted", workflowExecution.Workflow.ID, 1) + err = increaseStatisticsField(ctx, "workflow_executions_aborted", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) if err != nil { log.Printf("Failed to increase aborted execution stats: %s", err) } @@ -3069,7 +3070,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } } - err = increaseStatisticsField(ctx, "workflow_executions", workflow.ID, 1) + err = increaseStatisticsField(ctx, "workflow_executions", workflow.ID, 1, workflowExecution.ExecutionOrg) if err != nil { log.Printf("Failed to increase stats execution stats: %s", err) } @@ -4130,7 +4131,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { return } - err = increaseStatisticsField(ctx, "total_apps_deleted", fileId, 1) + err = increaseStatisticsField(ctx, "total_apps_deleted", fileId, 1, user.ActiveOrg.Id) if err != nil { log.Printf("Failed to increase total apps loaded stats: %s", err) } @@ -5706,12 +5707,12 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin continue } - err = increaseStatisticsField(ctx, "total_apps_created", workflowapp.ID, 1) + err = increaseStatisticsField(ctx, "total_apps_created", workflowapp.ID, 1, "") if err != nil { log.Printf("Failed to increase total apps created stats: %s", err) } - err = increaseStatisticsField(ctx, "total_apps_loaded", workflowapp.ID, 1) + err = increaseStatisticsField(ctx, "total_apps_loaded", workflowapp.ID, 1, "") if err != nil { log.Printf("Failed to increase total apps loaded stats: %s", err) } @@ -6185,7 +6186,7 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { } if len(hook.Workflows) > 0 { - err = increaseStatisticsField(ctx, "total_workflow_triggers", hook.Workflows[0], -1) + err = increaseStatisticsField(ctx, "total_workflow_triggers", hook.Workflows[0], -1, user.ActiveOrg.Id) if err != nil { log.Printf("Failed to increase total workflows: %s", err) } diff --git a/frontend/package.json b/frontend/package.json index a4c40596..b627526f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "shuffler", "homepage": "https://shuffler.io", - "version": "0.6.0", + "version": "0.8.3", "private": true, "dependencies": { "@material-ui/core": "^4.5.2", diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index ad857ad6..252af7ef 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -31,6 +31,9 @@ import { useTheme } from '@material-ui/core/styles'; import HandlePayment from './HandlePayment' import OrgHeader from '../components/OrgHeader' +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; +import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; +import DescriptionIcon from '@material-ui/icons/Description'; import PolymerIcon from '@material-ui/icons/Polymer'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; import CloseIcon from '@material-ui/icons/Close'; @@ -78,6 +81,7 @@ const Admin = (props) => { const [environments, setEnvironments] = React.useState([]); const [authentication, setAuthentication] = React.useState([]); const [schedules, setSchedules] = React.useState([]) + const [files, setFiles] = React.useState([]) const [selectedUser, setSelectedUser] = React.useState({}) const [newPassword, setNewPassword] = React.useState(""); const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false) @@ -550,6 +554,78 @@ const Admin = (props) => { }); } + const getFiles = () => { + fetch(globalUrl + "/api/v1/files", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!") + return + } + + return response.json() + }) + .then((responseJson) => { + console.log(responseJson) + setFiles(responseJson) + }) + .catch(error => { + alert.error(error.toString()) + }); + } + + const downloadFile = (file) => { + fetch(globalUrl + "/api/v1/files/"+file.id+"/content", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!") + return "" + } + + return response.text() + }) + .then((respdata) => { + if (respdata.length === 0) { + alert.error("Failed getting file") + return + } + + var blob = new Blob( [ respdata ], { + type: 'application/octet-stream' + }) + + var url = URL.createObjectURL( blob ) + var link = document.createElement( 'a' ) + link.setAttribute( 'href', url ) + link.setAttribute( 'download', `${file.filename}` ) + var event = document.createEvent( 'MouseEvents' ) + event.initMouseEvent( 'click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null) + link.dispatchEvent( event ) + + //return response.json() + }) + .then((responseJson) => { + //console.log(responseJson) + //setSchedules(responseJson) + }) + .catch(error => { + alert.error(error.toString()) + }); + } + const getSchedules = () => { fetch(globalUrl + "/api/v1/workflows/schedules", { method: 'GET', @@ -705,6 +781,48 @@ const Admin = (props) => { }); } + const setConfig = (event, newValue) => { + if (newValue === 1) { + getUsers() + } else if (newValue === 2) { + getAppAuthentication() + } else if (newValue === 3) { + getEnvironments() + } else if (newValue === 4) { + getSchedules() + } else if (newValue === 5) { + getFiles() + } else if (newValue === 6) { + getOrgs() + } + + if (newValue === 6) { + console.log("Should get apps for categories.") + } + + const views = { + 0: "organization", + 1: "users", + 2: "app_auth", + 3: "environments", + 4: "schedules", + 5: "files", + 6: "categories", + } + + //var theURL = window.location.pathname + //FIXME: Add url edits + //var theURL = window.location + //theURL.replace(`/${views[curTab]}`, `/${views[newValue]}`) + //window.history.pushState({"html":response.html,"pageTitle":response.pageTitle},"", urlPath); + + //console.log(newpath) + //window.location.pathame = newpath + + setModalUser({}) + setCurTab(newValue) + } + if (firstRequest) { setFirstRequest(false) @@ -720,13 +838,14 @@ const Admin = (props) => { "app_auth": 2, "environments": 3, "schedules": 4, - "categories": 5, + "files": 5, } if (props.match.params.key !== undefined) { const tmpitem = views[props.match.params.key] if (tmpitem !== undefined) { - setCurTab(tmpitem) + //setCurTab(tmpitem) + setConfig("", tmpitem) } } } @@ -1532,6 +1651,113 @@ const Admin = (props) => {
: null + const filesView = curTab === 5 ? +
+
+

Files

+ Files from Workflows. Learn more +
+ + + + + + + + + + + + {files === undefined || files === null ? null : files.map((file, index) => { + var bgColor = "#27292d" + if (index % 2 === 0) { + bgColor = "#1f2023" + } + + return ( + + + + + + + + + + + style={{minWidth: 100, maxWidth: 100, overflow: "hidden"}} + /> + + + + + { + downloadFile(file) + }}> + + + + style={{minWidth: 75, maxWidth: 75, overflow: "hidden"}} + /> + {/* + + + + */} + + ) + })} + +
+ : null + const schedulesView = curTab === 4 ?
@@ -1860,7 +2086,7 @@ const Admin = (props) => {
: null - const organizationsTab = curTab === 6 ? + const organizationsTab = curTab === 7 ?

Organizations

@@ -1941,7 +2167,7 @@ const Admin = (props) => {
: null - const hybridTab = curTab === 5 ? + const hybridTab = curTab === 6 ?

Hybrid

@@ -1983,48 +2209,11 @@ const Admin = (props) => { // primary={environment.Registered ? "true" : "false"} - const setConfig = (event, newValue) => { - if (newValue === 1) { - getUsers() - } else if (newValue === 2) { - getAppAuthentication() - } else if (newValue === 3) { - getEnvironments() - } else if (newValue === 4) { - getSchedules() - } else if (newValue === 6) { - getOrgs() - } - - if (newValue === 6) { - console.log("Should get apps for categories.") - } - - const views = { - 0: "organization", - 1: "users", - 2: "app_auth", - 3: "environments", - 4: "schedules", - 5: "categories", - } - - //var theURL = window.location.pathname - //FIXME: Add url edits - //var theURL = window.location - //theURL.replace(`/${views[curTab]}`, `/${views[newValue]}`) - //window.history.pushState({"html":response.html,"pageTitle":response.pageTitle},"", urlPath); - - //console.log(newpath) - //window.location.pathame = newpath - - setModalUser({}) - setCurTab(newValue) - } + const iconStyle = {marginRight: 10} const data = -
+
{ {isCloud ? null : App Authentication/>} {isCloud ? null : Environments/>} {isCloud ? null : Schedules />} + {isCloud ? null : Files />} {window.location.protocol == "http:" && window.location.port === "3000" ? Hybrid/> : null} {window.location.protocol == "http:" && window.location.port === "3000" ? Organizations/> : null} {window.location.protocol === "http:" && window.location.port === "3000" ? Categories/> : null} @@ -2049,6 +2239,7 @@ const Admin = (props) => { {usersView} {environmentView} {schedulesView} + {filesView} {hybridTab} {organizationsTab}
From 79784d322678acd37dd0096430bd1747ba46058e Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 18 Dec 2020 04:02:45 +0100 Subject: [PATCH 021/185] #216: App and workflow downloads can now be organization-wide controlled --- backend/go-app/main.go | 67 ++++++++++++++++++++++++++++++++ frontend/src/App.jsx | 2 +- frontend/src/views/Apps.jsx | 6 +-- frontend/src/views/Workflows.jsx | 6 +-- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 7257fa6f..c00389ee 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1727,6 +1727,14 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { } } + // FIXME: Remove this dependency by updating users' orgs when org itself is updated + org, err := getOrg(ctx, userInfo.ActiveOrg.Id) + if err == nil { + userInfo.ActiveOrg = *org + userInfo.ActiveOrg.Users = []User{} + } + + log.Printf("Org: %#v", userInfo.ActiveOrg.Defaults) currentOrg, err := json.Marshal(userInfo.ActiveOrg) if err != nil { currentOrg = []byte("{}") @@ -2604,6 +2612,10 @@ func setOrg(ctx context.Context, org Org, id string) error { return err } + // FIXME: Make this update every user to have the correct org data. + //org = fixOrgUser(ctx, &org) + //_ = org + return nil } @@ -2711,6 +2723,61 @@ func setEnvironment(ctx context.Context, data *Environment) error { return nil } +func fixOrgUser(ctx context.Context, org *Org) *Org { + //found := false + //for _, id := range user.Orgs { + // if user.ActiveOrg.Id == id { + // found = true + // break + // } + //} + + //if !found { + // user.Orgs = append(user.Orgs, user.ActiveOrg.Id) + //} + + //// Might be vulnerable to timing attacks. + //for _, orgId := range user.Orgs { + // if len(orgId) == 0 { + // continue + // } + + // org, err := getOrg(ctx, orgId) + // if err != nil { + // log.Printf("Error getting org %s", orgId) + // continue + // } + + // orgIndex := 0 + // userFound := false + // for index, orgUser := range org.Users { + // if orgUser.Id == user.Id { + // orgIndex = index + // userFound = true + // break + // } + // } + + // if userFound { + // user.PrivateApps = []WorkflowApp{} + // user.Executions = ExecutionInfo{} + // user.Limits = UserLimits{} + // user.Authentication = []UserAuth{} + + // org.Users[orgIndex] = *user + // } else { + // org.Users = append(org.Users, *user) + // } + + // err = setOrg(ctx, *org, orgId) + // if err != nil { + // log.Printf("Failed setting org %s", orgId) + // } + //} + + return org +} + func fixUserOrg(ctx context.Context, user *User) *User { found := false for _, id := range user.Orgs { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5bdda5b9..0864b984 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -142,7 +142,7 @@ const App = (message, props) => { } /> } /> } /> - } /> + } /> } /> } /> { window.location.pathname = "/docs/about" }} /> diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 3113bc31..dadbe288 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -106,7 +106,7 @@ export const GetParsedPaths = (inputdata, basekey) => { const Apps = (props) => { - const { globalUrl, isLoggedIn, isLoaded } = props; + const { globalUrl, isLoggedIn, isLoaded, userdata } = props; //const [workflows, setWorkflows] = React.useState([]); const baseRepository = "https://github.com/frikky/shuffle-apps" @@ -1316,7 +1316,7 @@ const Apps = (props) => { style={{backgroundColor: inputColor}} variant="outlined" margin="normal" - defaultValue="https://github.com/frikky/shuffle-apps" + defaultValue={userdata.active_org.defaults.app_download_repo !== undefined && userdata.active_org.defaults.app_download_repo.length > 0 ? userdata.active_org.defaults.app_download_repo : "https://github.com/frikky/shuffle-apps"} InputProps={{ style:{ color: "white", @@ -1334,7 +1334,7 @@ const Apps = (props) => { style={{backgroundColor: inputColor}} variant="outlined" margin="normal" - value={downloadBranch} + defaultValue={userdata.active_org.defaults.app_download_branch !== undefined && userdata.active_org.defaults.app_download_branch.length > 0 ? userdata.active_org.defaults.app_download_branch : downloadBranch} InputProps={{ style:{ color: "white", diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index c0387cb0..3b0ec8bc 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -44,7 +44,7 @@ const inputColor = "#383B40" const surfaceColor = "#27292D" const Workflows = (props) => { - const { globalUrl, isLoggedIn, isLoaded, removeCookie, cookies} = props; + const { globalUrl, isLoggedIn, isLoaded, removeCookie, cookies, userdata} = props; document.title = "Shuffle - Workflows" const alert = useAlert() @@ -1348,7 +1348,7 @@ const Workflows = (props) => { style={{backgroundColor: inputColor}} variant="outlined" margin="normal" - value={downloadUrl} + defaultValue={userdata.active_org.defaults.workflow_download_repo !== undefined && userdata.active_org.defaults.workflow_download_repo.length > 0 ? userdata.active_org.defaults.workflow_download_repo : downloadUrl} InputProps={{ style:{ color: "white", @@ -1367,7 +1367,7 @@ const Workflows = (props) => { style={{backgroundColor: inputColor}} variant="outlined" margin="normal" - value={downloadBranch} + defaultValue={userdata.active_org.defaults.workflow_download_branch !== undefined && userdata.active_org.defaults.workflow_download_branch.length > 0 ? userdata.active_org.defaults.workflow_download_branch : downloadBranch} InputProps={{ style:{ color: "white", From 4dd3e6be535257026430c2e1571831daf4b1d679 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 18 Dec 2020 10:26:23 +0100 Subject: [PATCH 022/185] BUG: User registered twice when created under org --- backend/go-app/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index c00389ee..d2229a78 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1140,7 +1140,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { neworg, err := getOrg(ctx, org.Id) if err == nil { - neworg.Users = append(neworg.Users, *newUser) + //neworg.Users = append(neworg.Users, *newUser) err = setOrg(ctx, *neworg, neworg.Id) if err != nil { log.Printf("Failed updating org with user %s", newUser.Username) @@ -1734,7 +1734,6 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { userInfo.ActiveOrg.Users = []User{} } - log.Printf("Org: %#v", userInfo.ActiveOrg.Defaults) currentOrg, err := json.Marshal(userInfo.ActiveOrg) if err != nil { currentOrg = []byte("{}") @@ -2397,6 +2396,10 @@ func handleGetUsers(resp http.ResponseWriter, request *http.Request) { continue } + //for _, tmpUser := range newUsers { + // if tmpUser.Name + //} + item.Password = "" item.Session = "" item.VerificationToken = "" From b5d16d7458615a234fd5b5d574c1077269df38e8 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 19 Dec 2020 06:57:18 +0100 Subject: [PATCH 023/185] BUG: Issue with cgroups in Docker on certain servers --- .env | 2 +- docker-compose.yml | 6 +++--- frontend/src/views/Admin.jsx | 26 +++++++++++++++++++++++--- functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/orborus.go | 19 +++++++++++++++++-- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/.env b/.env index a43c6669..5266d136 100644 --- a/.env +++ b/.env @@ -39,4 +39,4 @@ SHUFFLE_PASS_WORKER_PROXY=TRUE SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_NAME=frikky -SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.0" +SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.3" diff --git a/docker-compose.yml b/docker-compose.yml index f5f69046..d366dfce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,8 @@ services: depends_on: - backend backend: - #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.3 + build: ./backend + image: ghcr.io/frikky/shuffle-backend:0.8.4 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -45,7 +45,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.3 + image: ghcr.io/frikky/shuffle-orborus:0.8.31 container_name: shuffle-orborus hostname: shuffle-orborus networks: diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 252af7ef..e50a5a0c 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1582,8 +1582,13 @@ const Admin = (props) => { /> {users === undefined ? null : users.map((data, index) => { + var bgColor = "#27292d" + if (index % 2 === 0) { + bgColor = "#1f2023" + } + return ( - + { /> {schedules === undefined || schedules === null ? null : schedules.map((schedule, index) => { + var bgColor = "#27292d" + if (index % 2 === 0) { + bgColor = "#1f2023" + } + return ( - + {schedule.seconds} seconds} @@ -1938,8 +1948,13 @@ const Admin = (props) => { /> {authentication === undefined ? null : authentication.map((data, index) => { + var bgColor = "#27292d" + if (index % 2 === 0) { + bgColor = "#1f2023" + } + return ( - + style={{minWidth: 150, maxWidth: 150}} @@ -2046,6 +2061,11 @@ const Admin = (props) => { return null } + //var bgColor = "#27292d" + //if (index % 2 === 0) { + // bgColor = "#1f2023" + //} + return ( Date: Sat, 19 Dec 2020 08:06:03 +0100 Subject: [PATCH 024/185] BUG: Fixed an issue with Docker build references for older versions --- backend/go-app/docker.go | 28 ++++++++++++++++++++++++++++ backend/go-app/walkoff.go | 2 ++ frontend/src/views/AppCreator.jsx | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index b0398045..a95f4874 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -125,12 +125,25 @@ func getParsedTarMemory(fs billy.Filesystem, tw *tar.Writer, baseDir, extra stri return err } + log.Printf("FILENAME: %s", filename) readFile, err := ioutil.ReadAll(fileReader) if err != nil { log.Printf("Not file: %s", err) return err } + // Fixes issues with older versions of Docker for file format + if filename == "Dockerfile" { + log.Printf("Should search and replace in readfile.") + + referenceCheck := "FROM frikky/shuffle:" + if strings.Contains(string(readFile), referenceCheck) { + log.Printf("SHOULD SEARCH & REPLACE!") + newReference := fmt.Sprintf("FROM registry.hub.docker.com/frikky/shuffle:") + readFile = []byte(strings.Replace(string(readFile), referenceCheck, newReference, -1)) + } + } + //log.Printf("Filename: %s", filename) // FIXME - might need the folder from EXTRA here // Name has to be e.g. just "requirements.txt" @@ -156,6 +169,21 @@ func getParsedTarMemory(fs billy.Filesystem, tw *tar.Writer, baseDir, extra stri return nil } +/* +// Fixes App SDK issues.. meh +func fixTags(tags []string) []string { + checkTag := "frikky/shuffle" + newTags := []string{} + for _, tag := range tags { + if strings.HasPrefix(tag, checkTags) { + newTags.append(newTags, fmt.Sprintf("registry.hub.docker.com/%s", tag)) + } + + newTags.append(tag) + } +} +*/ + // Custom Docker image builder wrapper in memory func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder string) error { ctx := context.Background() diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 2045cf82..19634579 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -5541,6 +5541,8 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin } } + log.Printf("HANDLING DOCKER FILEREADER - SEARCH&REPLACE?") + appfileData, err := ioutil.ReadAll(fileReader) if err != nil { log.Printf("Failed reading %s: %s", fullPath, err) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index b8a2ee01..068d67fe 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1286,7 +1286,7 @@ const AppCreator = (props) => { : - + From 0ceea929e9634808d60b1144b779ca8fd04d592f Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 19 Dec 2020 09:56:19 +0100 Subject: [PATCH 025/185] BUG: Added remote download failover if build fails for a tag --- backend/go-app/docker.go | 53 ++++++++++++---- backend/go-app/walkoff.go | 6 +- docker-compose.yml | 4 +- frontend/src/views/AppCreator.jsx | 102 ++++++++++++++++-------------- frontend/src/views/LoginPage.jsx | 2 +- 5 files changed, 102 insertions(+), 65 deletions(-) diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index a95f4874..540f2cbe 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -125,24 +125,28 @@ func getParsedTarMemory(fs billy.Filesystem, tw *tar.Writer, baseDir, extra stri return err } - log.Printf("FILENAME: %s", filename) + //log.Printf("FILENAME: %s", filename) readFile, err := ioutil.ReadAll(fileReader) if err != nil { log.Printf("Not file: %s", err) return err } - // Fixes issues with older versions of Docker for file format - if filename == "Dockerfile" { - log.Printf("Should search and replace in readfile.") + // Fixes issues with older versions of Docker and reference formats + // Specific to Shuffle rn. Could expand. + // FIXME: Seems like the issue was with multi-stage builds + /* + if filename == "Dockerfile" { + log.Printf("Should search and replace in readfile.") - referenceCheck := "FROM frikky/shuffle:" - if strings.Contains(string(readFile), referenceCheck) { - log.Printf("SHOULD SEARCH & REPLACE!") - newReference := fmt.Sprintf("FROM registry.hub.docker.com/frikky/shuffle:") - readFile = []byte(strings.Replace(string(readFile), referenceCheck, newReference, -1)) + referenceCheck := "FROM frikky/shuffle:" + if strings.Contains(string(readFile), referenceCheck) { + log.Printf("SHOULD SEARCH & REPLACE!") + newReference := fmt.Sprintf("FROM registry.hub.docker.com/frikky/shuffle:") + readFile = []byte(strings.Replace(string(readFile), referenceCheck, newReference, -1)) + } } - } + */ //log.Printf("Filename: %s", filename) // FIXME - might need the folder from EXTRA here @@ -230,12 +234,39 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin dockerFileTarReader, buildOptions, ) + + log.Printf("Response: %#v", imageBuildResponse.Body) //log.Printf("IMAGERESPONSE: %#v", imageBuildResponse.Body) defer imageBuildResponse.Body.Close() - _, newerr := io.Copy(os.Stdout, imageBuildResponse.Body) + buildBuf := new(strings.Builder) + _, newerr := io.Copy(buildBuf, imageBuildResponse.Body) if newerr != nil { log.Printf("Failed reading Docker build STDOUT: %s", newerr) + } else { + if strings.Contains(buildBuf.String(), "errorDetail") { + log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n")) + + // Handles pulling of the same image if applicable + // This fixes some issues with older versions of Docker which can't build + // on their own ( <17.05 ) + pullOptions := types.ImagePullOptions{} + canonicalName := fmt.Sprintf("registry.hub.docker.com") + for _, image := range tags { + newImage := fmt.Sprintf("%s/%s", canonicalName, image) + log.Printf("[INFO] Pulling image %s", newImage) + reader, err := client.ImagePull(ctx, newImage, pullOptions) + if err != nil { + log.Printf("[ERROR] Failed getting image %s: %s", newImage, err) + continue + } + + //newBuf := buildBuf + io.Copy(os.Stdout, reader) + log.Printf("[INFO] Successfully downloaded and built %s", newImage) + } + //baseDockerName + } } if err != nil { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 19634579..54365d60 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -5541,7 +5541,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin } } - log.Printf("HANDLING DOCKER FILEREADER - SEARCH&REPLACE?") + //log.Printf("HANDLING DOCKER FILEREADER - SEARCH&REPLACE?") appfileData, err := ioutil.ReadAll(fileReader) if err != nil { @@ -5688,10 +5688,10 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin if len(removeApps) > 0 { for _, item := range removeApps { - log.Printf("Removing duplicate: %s", item) + log.Printf("[WARNING] Removing duplicate: %s", item) err = DeleteKey(ctx, "workflowapp", item) if err != nil { - log.Printf("Failed deleting %s", item) + log.Printf("[ERROR] Failed deleting duplicate %s: %s", item, err) } } } diff --git a/docker-compose.yml b/docker-compose.yml index d366dfce..a1936a20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.3 + image: ghcr.io/frikky/shuffle-frontend:0.8.4 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.4 + image: ghcr.io/frikky/shuffle-backend:0.8.42 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 068d67fe..3171610a 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -101,7 +101,12 @@ const parseCurl = (s) => { return "" } - var args = rewrite(words.split(s)) + try { + var args = rewrite(words.split(s)) + } catch (e) { + return s + } + var out = { method: 'GET', header: {} } var state = '' @@ -1731,64 +1736,65 @@ const AppCreator = (props) => { var parsedurl = event.target.value if (parsedurl.startsWith("curl")) { const request = parseCurl(event.target.value) - console.log(request) - if (request.method.toUpperCase() !== currentAction.Method) { - setCurrentActionMethod(request.method.toUpperCase()) - setActionField("method", request.method.toUpperCase()) - } + if (request !== event.target.value) { + if (request.method.toUpperCase() !== currentAction.Method) { + setCurrentActionMethod(request.method.toUpperCase()) + setActionField("method", request.method.toUpperCase()) + } - if (request.header !== undefined && request.header !== null) { - var headers = [] - for (let [key, value] of Object.entries(request.header)) { - if (parameterName !== undefined && key.toLowerCase() === parameterName.toLowerCase()) { - continue + if (request.header !== undefined && request.header !== null) { + var headers = [] + for (let [key, value] of Object.entries(request.header)) { + if (parameterName !== undefined && key.toLowerCase() === parameterName.toLowerCase()) { + continue + } + + if (key === "Authorization" && authenticationOption === "Bearer auth") { + continue + } + + headers += key+"="+value+"\n" } - if (key === "Authorization" && authenticationOption === "Bearer auth") { - continue - } - - headers += key+"="+value+"\n" + setActionField("headers", headers) } - setActionField("headers", headers) - } + if (request.body !== undefined && request.body !== null) { + setActionField("body", request.body) + } - if (request.body !== undefined && request.body !== null) { - setActionField("body", request.body) - } - - // Parse URL - if (request.url !== undefined) { - parsedurl = request.url - } + // Parse URL + if (request.url !== undefined) { + parsedurl = request.url + } } - if (parsedurl !== undefined) { - if (parsedurl.includes("<") && parsedurl.includes(">")) { - parsedurl = parsedurl.split("<").join("{") - parsedurl = parsedurl.split(">").join("}") - } - - if (parsedurl.startsWith("http") || parsedurl.startsWith("ftp")) { - if (parsedurl !== undefined && parsedurl.includes(parameterName)) { - // Remove <> etc. - // - - console.log("IT HAS THE PARAM NAME!") - const newurl = new URL(encodeURI(parsedurl)) - newurl.searchParams.delete(parameterName) - parsedurl = decodeURI(newurl.href) + if (parsedurl !== undefined) { + if (parsedurl.includes("<") && parsedurl.includes(">")) { + parsedurl = parsedurl.split("<").join("{") + parsedurl = parsedurl.split(">").join("}") } - // Remove the base URL itself - if (parsedurl !== undefined && baseUrl !== undefined && baseUrl.length > 0 && parsedurl.includes(baseUrl)) { - parsedurl = parsedurl.replace(baseUrl, "") - } + if (parsedurl.startsWith("http") || parsedurl.startsWith("ftp")) { + if (parsedurl !== undefined && parsedurl.includes(parameterName)) { + // Remove <> etc. + // + + console.log("IT HAS THE PARAM NAME!") + const newurl = new URL(encodeURI(parsedurl)) + newurl.searchParams.delete(parameterName) + parsedurl = decodeURI(newurl.href) + } - // Check URL query && headers - setActionField("url", parsedurl) - setUrlPath(parsedurl) + // Remove the base URL itself + if (parsedurl !== undefined && baseUrl !== undefined && baseUrl.length > 0 && parsedurl.includes(baseUrl)) { + parsedurl = parsedurl.replace(baseUrl, "") + } + + // Check URL query && headers + setActionField("url", parsedurl) + setUrlPath(parsedurl) + } } } diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index ef746a6d..5f9e49f2 100644 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -130,7 +130,7 @@ const LoginDialog = props => { if (responseJson["success"] === false) { setLoginInfo(responseJson["reason"]) } else { - setLoginInfo("Successful register :)") + setLoginInfo("Successful register!") } }), ) From e38950e285901df431bca1534c6a0a4d181c606a Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 19 Dec 2020 10:27:50 +0100 Subject: [PATCH 026/185] Added extension folder for Shuffle --- .../cortex-responders/Shuffle/shuffle.json | 35 +++++++++++++++ .../cortex-responders/Shuffle/shuffle.py | 28 ++++++++++++ functions/extensions/wazuh/ossec.conf | 7 +++ functions/extensions/wazuh/requirements.txt | 1 + functions/extensions/wazuh/shuffle.py | 44 +++++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 functions/extensions/cortex-responders/Shuffle/shuffle.json create mode 100644 functions/extensions/cortex-responders/Shuffle/shuffle.py create mode 100644 functions/extensions/wazuh/ossec.conf create mode 100644 functions/extensions/wazuh/requirements.txt create mode 100644 functions/extensions/wazuh/shuffle.py diff --git a/functions/extensions/cortex-responders/Shuffle/shuffle.json b/functions/extensions/cortex-responders/Shuffle/shuffle.json new file mode 100644 index 00000000..ef2610dd --- /dev/null +++ b/functions/extensions/cortex-responders/Shuffle/shuffle.json @@ -0,0 +1,35 @@ +{ + "name": "Shuffle", + "version": "1.0", + "author": "@frikkylikeme", + "url": "https://github.com/frikky/shuffle", + "license": "AGPL-V3", + "description": "Execute a workflow in Shuffle", + "dataTypeList": ["thehive:case", "thehive:alert"], + "command": "Shuffle/shuffle.py", + "baseConfig": "Shuffle", + "configurationItems": [ + { + "name": "url", + "description": "The URL to your shuffle instance", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://shuffler.io" + }, + { + "name": "api_key", + "description": "The API key to your Shuffle user", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "workflow_id", + "description": "The ID of the workflow to execute", + "type": "string", + "multi": false, + "required": true + } + ] +} diff --git a/functions/extensions/cortex-responders/Shuffle/shuffle.py b/functions/extensions/cortex-responders/Shuffle/shuffle.py new file mode 100644 index 00000000..0816ca53 --- /dev/null +++ b/functions/extensions/cortex-responders/Shuffle/shuffle.py @@ -0,0 +1,28 @@ + +#!/usr/bin/env python +# encoding: utf-8 + +from cortexutils.responder import Responder +import requests + +class Shuffle(Responder): + def __init__(self): + Responder.__init__(self) + self.api_key = self.get_param("config.api_key", "") + self.url = self.get_param("config.url", "") + self.workflow_id = self.get_param("config.workflow_id", "") + + def run(self): + Responder.run(self) + + parsed_url = "%s/api/v1/workflows/%s/execute" % (self.url, self.workflow_id) + headers = { + "Authorization": "Bearer %s" % self.api_key + } + requests.post(parsed_url, headers=headers) + + self.report({'message': 'message sent'}) + +if __name__ == '__main__': + Shuffle().run() + diff --git a/functions/extensions/wazuh/ossec.conf b/functions/extensions/wazuh/ossec.conf new file mode 100644 index 00000000..dfaf0394 --- /dev/null +++ b/functions/extensions/wazuh/ossec.conf @@ -0,0 +1,7 @@ + + Shuffle + http://:3001/api/v1/hooks/webhook_ + 2 + multiple_drops|authentication_failures + json + diff --git a/functions/extensions/wazuh/requirements.txt b/functions/extensions/wazuh/requirements.txt new file mode 100644 index 00000000..f2293605 --- /dev/null +++ b/functions/extensions/wazuh/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/functions/extensions/wazuh/shuffle.py b/functions/extensions/wazuh/shuffle.py new file mode 100644 index 00000000..306d3715 --- /dev/null +++ b/functions/extensions/wazuh/shuffle.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +# Based on +# https://wazuh.com/blog/how-to-integrate-external-software-using-integrator/ + +import sys +import json +import requests +from requests.auth import HTTPBasicAuth + +# Set the project attributes +project_alias = 'TI' +issue_name ='FIM' + +# Read configuration parameters +alert_file = open(sys.argv[1]) +user = sys.argv[2].split(':')[0] +api_key = sys.argv[2].split(':')[1] +hook_url = sys.argv[3] + +# Read the alert file +alert_json = json.loads(alert_file.read()) +alert_file.close() + +# Extract issue fields +alert_level = alert_json['rule']['level'] +description = alert_json['rule']['description'] +path = alert_json['syscheck']['path'] + +# Generate request +msg_data = {} +msg_data['fields'] = {} +msg_data['fields']['project'] = {} +msg_data['fields']['project']['key'] = project_alias +msg_data['fields']['summary'] = 'FIM alert on [' + path + ']' +msg_data['fields']['description'] = '- State: ' + description + '\n- Alert level: ' + str(alert_level) +msg_data['fields']['issuetype'] = {} +msg_data['fields']['issuetype']['name'] = issue_name +headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} + +# Send the request +requests.post(hook_url, data=json.dumps(msg_data), headers=headers, auth=(user, api_key)) + +sys.exit(0) From 2248e407f76e80ac983dd6b6419fd9e10181a511 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 19 Dec 2020 14:21:39 +0100 Subject: [PATCH 027/185] BUG: Docker build issue for old version workaround --- backend/go-app/docker.go | 25 +++++++++++++--- backend/go-app/main.go | 3 +- backend/go-app/walkoff.go | 4 +-- docker-compose.yml | 8 +++--- functions/onprem/worker/worker.go | 48 ++++++++++++++++++++++++++----- 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 540f2cbe..3d48871c 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -235,7 +236,7 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin buildOptions, ) - log.Printf("Response: %#v", imageBuildResponse.Body) + //log.Printf("Response: %#v", imageBuildResponse.Body) //log.Printf("IMAGERESPONSE: %#v", imageBuildResponse.Body) defer imageBuildResponse.Body.Close() @@ -251,9 +252,12 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin // This fixes some issues with older versions of Docker which can't build // on their own ( <17.05 ) pullOptions := types.ImagePullOptions{} - canonicalName := fmt.Sprintf("registry.hub.docker.com") + downloaded := false for _, image := range tags { - newImage := fmt.Sprintf("%s/%s", canonicalName, image) + // Is this ok? Not sure. Tags shouldn't be controlled here prolly. + image = strings.ToLower(image) + + newImage := fmt.Sprintf("%s/%s", registryName, image) log.Printf("[INFO] Pulling image %s", newImage) reader, err := client.ImagePull(ctx, newImage, pullOptions) if err != nil { @@ -261,10 +265,17 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin continue } + // Attempt to retag the image to not contain registry... + //newBuf := buildBuf + downloaded = true io.Copy(os.Stdout, reader) log.Printf("[INFO] Successfully downloaded and built %s", newImage) } + + if !downloaded { + return errors.New(fmt.Sprintf("Failed to build / download images %s", strings.Join(tags, ","))) + } //baseDockerName } } @@ -331,9 +342,15 @@ func buildImage(tags []string, dockerfileFolder string) error { // Read the STDOUT from the build process defer imageBuildResponse.Body.Close() - _, err = io.Copy(os.Stdout, imageBuildResponse.Body) + buildBuf := new(strings.Builder) + _, err = io.Copy(buildBuf, imageBuildResponse.Body) if err != nil { return err + } else { + if strings.Contains(buildBuf.String(), "errorDetail") { + log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), strings.Join(tags, "\n")) + return errors.New(fmt.Sprintf("Failed building %s. Check backend logs for details. Most likely means you have an old version of Docker.", strings.Join(tags, ","))) + } } return nil diff --git a/backend/go-app/main.go b/backend/go-app/main.go index d2229a78..b29e9fcf 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -72,6 +72,7 @@ var gceProject = "shuffle" var bucketName = "shuffler.appspot.com" var baseAppPath = "/home/frikky/git/shaffuru/tmp/apps" var baseDockerName = "frikky/shuffle" +var registryName = "registry.hub.docker.com" //var syncUrl = "http://192.168.102.54:5002" var syncUrl = "https://shuffler.io" @@ -6484,7 +6485,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { dockerLocation := fmt.Sprintf("%s/Dockerfile", basePath) log.Printf("Dockerfile: %s", dockerLocation) - versionName := fmt.Sprintf("%s_%s", strings.ReplaceAll(api.Name, " ", "-"), api.AppVersion) + versionName := fmt.Sprintf("%s_%s", strings.ToLower(strings.ReplaceAll(api.Name, " ", "-")), api.AppVersion) dockerTags := []string{ fmt.Sprintf("%s:%s", baseDockerName, identifier), fmt.Sprintf("%s:%s", baseDockerName, versionName), diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 54365d60..448d677c 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -5599,7 +5599,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin newName = strings.ReplaceAll(newName, " ", "-") tags := []string{ - fmt.Sprintf("%s:%s_%s", baseDockerName, newName, workflowapp.AppVersion), + fmt.Sprintf("%s:%s_%s", baseDockerName, strings.ToLower(newName), workflowapp.AppVersion), } if len(allapps) == 0 { @@ -5673,7 +5673,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin } if len(appendParams) > 0 { - log.Printf("Appending %d params to the START of %s", len(appendParams), action.Name) + log.Printf("[AUTH] Appending %d params to the START of %s", len(appendParams), action.Name) workflowapp.Actions[index].Parameters = append(appendParams, workflowapp.Actions[index].Parameters...) } diff --git a/docker-compose.yml b/docker-compose.yml index a1936a20..a39b58c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,8 @@ services: depends_on: - backend backend: - build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.42 + #build: ./backend + image: ghcr.io/frikky/shuffle-backend:0.8.43 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -53,8 +53,8 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - - SHUFFLE_APP_SDK_VERSION=0.8.0 - - SHUFFLE_WORKER_VERSION=0.8.0 + - SHUFFLE_APP_SDK_VERSION=0.8.3 + - SHUFFLE_WORKER_VERSION=0.8.3 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 2f9dfe55..067cdbe4 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -23,6 +23,8 @@ import ( var environment = os.Getenv("ENVIRONMENT_NAME") var baseUrl = os.Getenv("BASE_URL") var baseimagename = "frikky/shuffle" +var registryName = "registry.hub.docker.com" +var fallbackName = "shuffle-orborus" var sleepTime = 2 var containerId string @@ -34,6 +36,11 @@ func getThisContainerId() string { out, err := exec.Command("bash", "-c", cmd).Output() if err == nil { id = strings.TrimSpace(string(out)) + + log.Printf("Checking if %s is in %s", ".scope", string(out)) + if strings.Contains(string(out), ".scope") { + id = fallbackName + } } return id @@ -42,7 +49,7 @@ func getThisContainerId() string { func init() { containerId = getThisContainerId() if len(containerId) == 0 { - log.Printf("[ERROR] No container ID found.") + log.Printf("[ERROR] No container ID found. Not running containerized?") } else { log.Printf("[INFO] Found container ID: %s", containerId) } @@ -854,11 +861,17 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] ) if err != nil { - log.Printf("Container error: %s", err) + log.Printf("Container CREATE error: %s", err) + return err + } + + err = cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) + if err != nil { + log.Printf("[ERROR] Failed to start container in environment %s: %s", environment, err) + //shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) return err } - cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) log.Printf("[INFO] Container %s is created", cont.ID) return nil } @@ -1357,12 +1370,33 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W log.Printf("Skipping FULL_EXECUTION because size is larger than %d", maxSize) } + // Try original -> Go to lowercase err = deployApp(dockercli, image, identifier, env) if err != nil { - log.Printf("[ERROR] Failed deploying %s from image %s: %s", identifier, image, err) - if strings.Contains(err.Error(), "No such image") { - log.Printf("[ERROR] Image doesn't exist. Shutting down") - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + // Trying to replace with lowercase to deploy again. This seems to work with Dockerhub well. + // FIXME: Should try to remotely download directly if this persists. + image = fmt.Sprintf("%s:%s_%s", baseimagename, strings.ToLower(action.AppName), action.AppVersion) + if strings.Contains(image, " ") { + image = strings.ReplaceAll(image, " ", "-") + } + + err = deployApp(dockercli, image, identifier, env) + if err != nil { + image = fmt.Sprintf("%s/%s:%s_%s", registryName, baseimagename, strings.ToLower(action.AppName), action.AppVersion) + if strings.Contains(image, " ") { + image = strings.ReplaceAll(image, " ", "-") + } + + err = deployApp(dockercli, image, identifier, env) + if err != nil { + + log.Printf("[ERROR] Failed deploying image THRICE. Aborting if the image doesn't exist") + if strings.Contains(err.Error(), "No such image") { + //log.Printf("[WARNING] Failed deploying %s from image %s: %s", identifier, image, err) + log.Printf("[ERROR] Image doesn't exist. Shutting down") + shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + } + } } } From 8a99fb60480d275562bdfc9ec3cccfd8a50a0233 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 22 Dec 2020 11:41:11 +0100 Subject: [PATCH 028/185] FIXES: Added wazuh integration and loads more --- backend/app_sdk/app_base.py | 51 ++++-- backend/app_sdk/build.sh | 2 +- backend/go-app/docker.go | 1 + backend/go-app/main.go | 14 +- backend/go-app/walkoff.go | 6 +- docker-compose.yml | 4 +- frontend/src/views/Admin.jsx | 28 +++ frontend/src/views/AngularWorkflow.jsx | 40 ++++- frontend/src/views/Apps.jsx | 4 + frontend/src/views/LoginPage.jsx | 5 +- functions/extensions/wazuh/custom-shuffle | 36 ++++ functions/extensions/wazuh/custom-shuffle.py | 177 +++++++++++++++++++ functions/extensions/wazuh/ossec.conf | 4 +- functions/extensions/wazuh/requirements.txt | 1 - functions/extensions/wazuh/shuffle.py | 44 ----- functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/orborus.go | 7 +- 17 files changed, 333 insertions(+), 93 deletions(-) create mode 100644 functions/extensions/wazuh/custom-shuffle create mode 100644 functions/extensions/wazuh/custom-shuffle.py delete mode 100644 functions/extensions/wazuh/requirements.txt delete mode 100644 functions/extensions/wazuh/shuffle.py diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index fd78155f..e226eefd 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -606,11 +606,14 @@ class AppBase: if "len" in thistype or "length" in thistype or "lenght" in thistype: tmp = "" try: - tmpdata = data.replace("\'", "\"") tmp = json.loads(tmpdata) except: - print("Passing bug") - pass + try: + tmpdata = data.replace("\'", "\"") + tmp = json.loads(tmpdata) + except: + print("[ERROR] Parsing bug for length in app sdk") + pass if isinstance(tmp, list): return len(tmp) @@ -649,14 +652,14 @@ class AppBase: return tmp except IndexError as e: return default_error - + # Parses the INNER value and recurses until everything is done def parse_wrapper(data): try: if "(" not in data or ")" not in data: - return data + return (data, False) except TypeError: - return data + return (data, False) #print("Running %s" % data) @@ -671,7 +674,7 @@ class AppBase: break if not found: - return data + return (data, False) # Do stuff here. innervalue = parse_nested_param(data, maxDepth(data)-0) @@ -692,10 +695,11 @@ class AppBase: parsed_value = parse_type(innervalue[0], thistype.lower()) print("Parsed value from %s: %s" % (thistype, parsed_value)) - return parsed_value + return (parsed_value, True) print("DATA: %s\n" % data) - return parse_wrapper(data) + return (parse_wrapper(data)[0], True) + # Looks for parantheses to grab special cases within a string, e.g: # int(1) lower(HELLO) or length(what's the length) @@ -737,15 +741,20 @@ class AppBase: if len(newstring) > 0: newdata.append(newstring) - print("Newdata: ", newdata) parsedlist = [] non_string = False + parsed = False for item in newdata: ret = parse_wrapper(item) - if not isinstance(ret, str): + if not isinstance(ret[0], str): non_string = True - - parsedlist.append(ret) + + parsedlist.append(ret[0]) + if ret[1]: + parsed = True + + if not parsed: + return data if len(parsedlist) > 0 and not non_string: print("Returning parsed list: ", parsedlist) @@ -949,7 +958,6 @@ class AppBase: if len(parsersplit) == 1: return str(baseresult)+str(appendresult), False - baseresult = baseresult.replace("\'", "\"") baseresult = baseresult.replace(" True,", " true,") baseresult = baseresult.replace(" False", " false,") @@ -958,8 +966,12 @@ class AppBase: try: basejson = json.loads(baseresult) except json.decoder.JSONDecodeError as e: - print("Parser issue with JSON: %s" % e) - return str(baseresult)+str(appendresult), False + try: + baseresult = baseresult.replace("\'", "\"") + basejson = json.loads(baseresult) + except json.decoder.JSONDecodeError as e: + print("Parser issue with JSON: %s" % e) + return str(baseresult)+str(appendresult), False print("After fourth parser return as JSON") @@ -1365,14 +1377,17 @@ class AppBase: if replacement.startswith("\"") and replacement.endswith("\""): replacement = replacement[1:len(replacement)-1] - replacement = replacement.replace("\'", "\"", -1) print("POST replacement: %s" % replacement) json_replacement = replacement try: json_replacement = json.loads(replacement) except json.decoder.JSONDecodeError as e: - print("JSON error singular: %s" % e) + try: + replacement = replacement.replace("\'", "\"", -1) + json_replacement = json.loads(replacement) + except: + print("JSON error singular: %s" % e) if len(json_replacement) > minlength: minlength = len(json_replacement) diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index fe988a1c..f96da989 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.32 +VERSION=0.8.4 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 3d48871c..03720e3b 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -230,6 +230,7 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin } // Build the actual image + log.Printf("Building %s. This may take up to a few minutes.", dockerfileFolder) imageBuildResponse, err := client.ImageBuild( ctx, dockerFileTarReader, diff --git a/backend/go-app/main.go b/backend/go-app/main.go index b29e9fcf..263e0872 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -2483,7 +2483,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { if len(users) != 1 { log.Printf(`Found multiple or no users with the same username: %s: %d`, data.Username, len(users)) resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found %d users with the same username: %s"}`, len(users), data.Username))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %d users with username %s"}`, len(users), data.Username))) return } @@ -4785,7 +4785,7 @@ func getDocList(resp http.ResponseWriter, request *http.Request) { if len(item1) == 0 { resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No docs available."`))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No docs available."}`))) return } @@ -4844,7 +4844,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) { location := strings.Split(request.URL.String(), "/") if len(location) != 5 { resp.WriteHeader(404) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"`))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"}`))) return } @@ -4868,7 +4868,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) { ) if err != nil { - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"`))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"}`))) resp.WriteHeader(404) //setBadMemcache(ctx, docPath) return @@ -4877,7 +4877,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) { newresp, err := client.Do(req) if err != nil { resp.WriteHeader(404) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"`))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"}`))) //setBadMemcache(ctx, docPath) return } @@ -4885,7 +4885,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) { body, err := ioutil.ReadAll(newresp.Body) if err != nil { resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't parse data"`))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't parse data"}`))) //setBadMemcache(ctx, docPath) return } @@ -5958,7 +5958,7 @@ func echoOpenapiData(resp http.ResponseWriter, request *http.Request) { if err != nil { log.Printf("[ERROR] URLbody error: %s", err) resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't get data from selected uri"`))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't get data from selected uri"}`))) return } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 448d677c..d2d99d56 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -4015,7 +4015,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { log.Printf("ID: %s", fileId) app, err := getApp(ctx, fileId) if err != nil { - log.Printf("Error getting app %s: %s", app.Name, err) + log.Printf("Error getting app (delete) %s: %s", fileId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4169,7 +4169,7 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() app, err := getApp(ctx, fileId) if err != nil { - log.Printf("Error getting app: %s", app.Name) + log.Printf("Error getting app (app config): %s", fileId) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4438,7 +4438,7 @@ func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() app, err := getApp(ctx, fileId) if err != nil { - log.Printf("Error getting app: %s (update app)", app.Name) + log.Printf("Error getting app (update app): %s", fileId) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return diff --git a/docker-compose.yml b/docker-compose.yml index a39b58c7..7a356dff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.4 + image: ghcr.io/frikky/shuffle-frontend:0.8.42 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -45,7 +45,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.31 + image: ghcr.io/frikky/shuffle-orborus:0.8.32 container_name: shuffle-orborus hostname: shuffle-orborus networks: diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index e50a5a0c..b809cf62 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -470,6 +470,33 @@ const Admin = (props) => { }) } + const flushQueue = (name) => { + // Just use this one? + const url = globalUrl + '/api/v1/flush_queue'; + fetch(url, { + method: 'DELETE', + credentials: "include", + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + alert.error(responseJson.reason) + getEnvironments() + } else { + setLoginInfo("") + setModalOpen(false) + getEnvironments() + } + }), + ) + .catch(error => { + console.log("Error when deleting: ", error) + }) + } + const deleteEnvironment = (name) => { // FIXME - add some check here ROFL alert.info("Deleting environment " + name) @@ -2094,6 +2121,7 @@ const Admin = (props) => { style={{minWidth: 150, maxWidth: 150, overflow: "hidden"}} > + {/**/} { const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false //const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" - const triggerEnvironments = isCloud ? ["cloud"] : ["cloud", "onprem"] + const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"] const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?' useBeforeunload(() => { @@ -2663,7 +2663,6 @@ const AngularWorkflow = (props) => { foundResult.result = foundResult.result.split(" None").join(" \"None\"") foundResult.result = foundResult.result.split(" False").join(" false") foundResult.result = foundResult.result.split(" True").join(" true") - foundResult.result = foundResult.result.split("\'").join("\"") var jsonvalid = true try { @@ -2672,7 +2671,15 @@ const AngularWorkflow = (props) => { jsonvalid = false } } catch (e) { - jsonvalid = false + try { + foundResult.result = foundResult.result.split("\'").join("\"") + const tmp = String(JSON.parse(foundResult.result)) + if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } } // Finds the FIRST json only @@ -5874,7 +5881,6 @@ const AngularWorkflow = (props) => { const parsedExecutionArgument = () => { var showResult = executionData.execution_argument.trim() showResult = showResult.split(" None").join(" \"None\"") - showResult = showResult.split("\'").join("\"") showResult = showResult.split(" False").join(" false") showResult = showResult.split(" True").join(" true") @@ -5885,7 +5891,16 @@ const AngularWorkflow = (props) => { jsonvalid = false } } catch (e) { - jsonvalid = false + showResult = showResult.split("\'").join("\"") + + try { + const tmp = String(JSON.parse(showResult)) + if (!showResult.includes("{") && !showResult.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } } if (jsonvalid) { @@ -6070,21 +6085,28 @@ const AngularWorkflow = (props) => { // // FIXME: The latter replace doens't really work if ' is used in a string var showResult = data.result.trim() + //console.log(showResult) showResult = showResult.split(" None").join(" \"None\"") showResult = showResult.split(" False").join(" false") showResult = showResult.split(" True").join(" true") - showResult = showResult.split("\'").join("\"") var jsonvalid = true try { const tmp = String(JSON.parse(showResult)) if (!showResult.includes("{") && !showResult.includes("[")) { - //console.log("IN HERE: ", tmp) jsonvalid = false } } catch (e) { - //console.log("Error: ", e) - jsonvalid = false + showResult = showResult.split("\'").join("\"") + + try { + const tmp = String(JSON.parse(showResult)) + if (!showResult.includes("{") && !showResult.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } } const curapp = apps.find(a => a.name === data.action.app_name && a.app_version === data.action.app_version) diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index dadbe288..c081f94f 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -46,6 +46,10 @@ const inputColor = "#383B40" export const GetParsedPaths = (inputdata, basekey) => { const splitkey = " > " var parsedValues = [] + if (inputdata === undefined || inputdata === null) { + return parsedValues + } + if (typeof(inputdata) !== "object") { return parsedValues } diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index 5f9e49f2..b365336f 100644 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -69,7 +69,7 @@ const LoginDialog = props => { }), ) .catch(error => { - setLoginInfo("Error in userdata: ", error) + setLoginInfo("Error logging in: ", error) }) } @@ -80,6 +80,7 @@ const LoginDialog = props => { const onSubmit = (e) => { e.preventDefault() + setLoginInfo("") // FIXME - add some check here ROFL // Just use this one? @@ -114,7 +115,7 @@ const LoginDialog = props => { }), ) .catch(error => { - setLoginInfo("Error in userdata: " + error) + setLoginInfo("Error logging in: " + error) }); } else { url = baseurl + '/api/v1/users/register'; diff --git a/functions/extensions/wazuh/custom-shuffle b/functions/extensions/wazuh/custom-shuffle new file mode 100644 index 00000000..bd540414 --- /dev/null +++ b/functions/extensions/wazuh/custom-shuffle @@ -0,0 +1,36 @@ +#!/bin/sh +# Created by Shuffle, AS. . + +WPYTHON_BIN="framework/python/bin/python3" + +SCRIPT_PATH_NAME="$0" + +DIR_NAME="$(cd $(dirname ${SCRIPT_PATH_NAME}); pwd -P)" +SCRIPT_NAME="$(basename ${SCRIPT_PATH_NAME})" + +case ${DIR_NAME} in + */active-response/bin | */wodles*) + if [ -z "${WAZUH_PATH}" ]; then + WAZUH_PATH="$(cd ${DIR_NAME}/../..; pwd)" + fi + + PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py" + ;; + */bin) + if [ -z "${WAZUH_PATH}" ]; then + WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)" + fi + + PYTHON_SCRIPT="${WAZUH_PATH}/framework/scripts/${SCRIPT_NAME}.py" + ;; + */integrations) + if [ -z "${WAZUH_PATH}" ]; then + WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)" + fi + + PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py" + ;; +esac + + +${WAZUH_PATH}/${WPYTHON_BIN} ${PYTHON_SCRIPT} "$@" diff --git a/functions/extensions/wazuh/custom-shuffle.py b/functions/extensions/wazuh/custom-shuffle.py new file mode 100644 index 00000000..06fa4c7d --- /dev/null +++ b/functions/extensions/wazuh/custom-shuffle.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# Created by Shuffle, AS. . +# Based on the Slack integration using Webhooks + +import json +import sys +import time +import os + +try: + import requests + from requests.auth import HTTPBasicAuth +except Exception as e: + print("No module 'requests' found. Install: pip install requests") + sys.exit(1) + +# ADD THIS TO ossec.conf configuration: +# +# custom-shuffle +# http://:3001/api/v1/hooks/ +# 3 +# json +# + +# Global vars + +debug_enabled = False +pwd = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +json_alert = {} +now = time.strftime("%a %b %d %H:%M:%S %Z %Y") + +# Set paths +log_file = '{0}/logs/integrations.log'.format(pwd) + + +def main(args): + debug("# Starting") + + # Read args + alert_file_location = args[1] + webhook = args[3] + + debug("# Webhook") + debug(webhook) + + debug("# File location") + debug(alert_file_location) + + # Load alert. Parse JSON object. + with open(alert_file_location) as alert_file: + json_alert = json.load(alert_file) + debug("# Processing alert") + debug(json_alert) + + debug("# Generating message") + msg = generate_msg(json_alert) + if isinstance(msg, str): + if len(msg) == 0: + return + debug(msg) + + debug("# Sending message") + send_msg(msg, webhook) + + +def debug(msg): + if debug_enabled: + msg = "{0}: {1}\n".format(now, msg) + print(msg) + f = open(log_file, "a") + f.write(msg) + f.close() + +# Skips container kills to stop self-recursion +def filter_msg(alert): + # These are things that recursively happen because Shuffle starts Docker containers + # Docker integration rules: https://github.com/wazuh/wazuh-ruleset/blob/ae36745db1d3f312db0392f5925c2f2b0ec009a9/rules/0560-docker_integration_rules.xml + skip = ["87924", "87900", "87901", "87902", "87903", "87904", "86001", "86002", "86003", "87932", "80710", "87929", "87928",] + if alert["rule"]["id"] in skip: + return False + + #try: + # if "docker" in alert["rule"]["description"].lower() and " + #msg['text'] = alert.get('full_log') + #except: + # pass + #msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A" + + return True + +def generate_msg(alert): + if not filter_msg(alert): + print("Skipping rule %s" % alert["rule"]["id"]) + return "" + + level = alert['rule']['level'] + + if (level <= 4): + color = "good" + elif (level >= 5 and level <= 7): + color = "warning" + else: + color = "danger" + + msg = {} + msg['color'] = color + msg['pretext'] = "WAZUH Alert" + msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A" + msg['text'] = alert.get('full_log') + msg['rule_id'] = alert["rule"]["id"] + msg['timestamp'] = alert["timestamp"] + msg['id'] = alert['id'] + msg["all_fields"] = alert + + #msg['fields'] = [] + # msg['fields'].append({ + # "title": "Agent", + # "value": "({0}) - {1}".format( + # alert['agent']['id'], + # alert['agent']['name'] + # ), + # }) + #if 'agentless' in alert: + # msg['fields'].append({ + # "title": "Agentless Host", + # "value": alert['agentless']['host'], + # }) + + #msg['fields'].append({"title": "Location", "value": alert['location']}) + #msg['fields'].append({ + # "title": "Rule ID", + # "value": "{0} _(Level {1})_".format(alert['rule']['id'], level), + #}) + + #attach = {'attachments': [msg]} + + return json.dumps(msg) + + +def send_msg(msg, url): + headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} + res = requests.post(url, data=msg, headers=headers) + debug(res) + + +if __name__ == "__main__": + try: + # Read arguments + bad_arguments = False + if len(sys.argv) >= 4: + msg = '{0} {1} {2} {3} {4}'.format( + now, + sys.argv[1], + sys.argv[2], + sys.argv[3], + sys.argv[4] if len(sys.argv) > 4 else '', + ) + debug_enabled = (len(sys.argv) > 4 and sys.argv[4] == 'debug') + else: + msg = '{0} Wrong arguments'.format(now) + bad_arguments = True + + # Logging the call + f = open(log_file, 'a') + f.write(msg + '\n') + f.close() + + if bad_arguments: + debug("# Exiting: Bad arguments.") + sys.exit(1) + + # Main function + main(sys.argv) + + except Exception as e: + debug(str(e)) + raise diff --git a/functions/extensions/wazuh/ossec.conf b/functions/extensions/wazuh/ossec.conf index dfaf0394..5b55f5d3 100644 --- a/functions/extensions/wazuh/ossec.conf +++ b/functions/extensions/wazuh/ossec.conf @@ -1,7 +1,5 @@ - Shuffle + custom-shuffle http://:3001/api/v1/hooks/webhook_ - 2 - multiple_drops|authentication_failures json diff --git a/functions/extensions/wazuh/requirements.txt b/functions/extensions/wazuh/requirements.txt deleted file mode 100644 index f2293605..00000000 --- a/functions/extensions/wazuh/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests diff --git a/functions/extensions/wazuh/shuffle.py b/functions/extensions/wazuh/shuffle.py deleted file mode 100644 index 306d3715..00000000 --- a/functions/extensions/wazuh/shuffle.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python - -# Based on -# https://wazuh.com/blog/how-to-integrate-external-software-using-integrator/ - -import sys -import json -import requests -from requests.auth import HTTPBasicAuth - -# Set the project attributes -project_alias = 'TI' -issue_name ='FIM' - -# Read configuration parameters -alert_file = open(sys.argv[1]) -user = sys.argv[2].split(':')[0] -api_key = sys.argv[2].split(':')[1] -hook_url = sys.argv[3] - -# Read the alert file -alert_json = json.loads(alert_file.read()) -alert_file.close() - -# Extract issue fields -alert_level = alert_json['rule']['level'] -description = alert_json['rule']['description'] -path = alert_json['syscheck']['path'] - -# Generate request -msg_data = {} -msg_data['fields'] = {} -msg_data['fields']['project'] = {} -msg_data['fields']['project']['key'] = project_alias -msg_data['fields']['summary'] = 'FIM alert on [' + path + ']' -msg_data['fields']['description'] = '- State: ' + description + '\n- Alert level: ' + str(alert_level) -msg_data['fields']['issuetype'] = {} -msg_data['fields']['issuetype']['name'] = issue_name -headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} - -# Send the request -requests.post(hook_url, data=json.dumps(msg_data), headers=headers, auth=(user, api_key)) - -sys.exit(0) diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index a580aefc..57fe63a2 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.31 +VERSION=0.8.32 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 8e5cf442..06a938ff 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -562,6 +562,7 @@ func zombiecheck(workerTimeout int) error { stopContainers := []string{} removeContainers := []string{} + log.Printf("Workertimeout: %d", int64(workerTimeout)) for _, container := range containers { // Skip random containers. Only handle things related to Shuffle. if !strings.Contains(container.Image, baseimagename) { @@ -587,10 +588,10 @@ func zombiecheck(workerTimeout int) error { continue } - log.Printf("[INFO] NAME: %s", name) + currenttime := time.Now().Unix() + log.Printf("[INFO] (%s) NAME: %s. TIME: %d", container.State, name, currenttime-container.Created) // Need to check time here too because a container can be removed the same instant as its created - currenttime := time.Now().Unix() if container.State != "running" && currenttime-container.Created > int64(workerTimeout) { removeContainers = append(removeContainers, container.ID) containerNames[container.ID] = name @@ -606,6 +607,7 @@ func zombiecheck(workerTimeout int) error { } // FIXME - add killing of apps with same execution ID too + log.Printf("[INFO] Should STOP %d containers.", len(stopContainers)) for _, containername := range stopContainers { log.Printf("[INFO] Stopping and removing container %s", containerNames[containername]) go dockercli.ContainerStop(ctx, containername, nil) @@ -617,6 +619,7 @@ func zombiecheck(workerTimeout int) error { Force: true, } + log.Printf("[INFO] Should REMOVE %d containers.", len(removeContainers)) for _, containername := range removeContainers { go dockercli.ContainerRemove(ctx, containername, removeOptions) } From c2cf104700bfb8b439922164b960cba7a294220f Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 24 Dec 2020 07:35:27 +0100 Subject: [PATCH 029/185] Feature: Added contains_any_of to conditions --- backend/app_sdk/app_base.py | 16 +++++++++ backend/go-app/walkoff.go | 14 ++++---- docker-compose.yml | 2 +- frontend/src/components/OrgHeader.js | 2 +- frontend/src/views/AngularWorkflow.jsx | 5 +++ functions/onprem/orborus/orborus.go | 2 +- functions/onprem/worker/worker.go | 45 +++++++++++++++++++++----- 7 files changed, 69 insertions(+), 17 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index e226eefd..98f62feb 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1127,6 +1127,22 @@ class AppBase: elif check.lower() == "contains": if destinationvalue.lower() in sourcevalue.lower(): return True + elif check.lower() == "contains_any_of": + newvalue = [destinationvalue.lower()] + if "," in destinationvalue: + newvalue = destinationvalue.split(",") + elif ", " in destinationvalue: + newvalue = destinationvalue.split(", ") + + for item in new_value: + if not item: + continue + + if item.trim() in sourcevalue: + print("[INFO] Found %s in %s" % (item, sourcevalue)) + return True + + return False elif check.lower() == "larger than": try: if sourcevalue.isdigit() and destinationvalue.isdigit(): diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index d2d99d56..85796a74 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -785,7 +785,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflowExecution, err := getWorkflowExecution(ctx, actionResult.ExecutionId) if err != nil { - log.Printf("Failed getting execution (streamresult) %s: %s", actionResult.ExecutionId, err) + //log.Printf("Failed getting execution (streamresult) %s: %s", actionResult.ExecutionId, err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key or execution_id might not exist."}`))) return @@ -1004,7 +1004,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } if actionResult.Status == "ABORTED" || actionResult.Status == "FAILURE" { - log.Printf("Actionresult is %s. Should set workflowExecution and exit all running functions", actionResult.Status) + log.Printf("[WARNING] Actionresult is %s. Should set workflowExecution and exit all running functions", actionResult.Status) newResults := []ActionResult{} childNodes := []string{} @@ -1016,7 +1016,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Finds ALL childnodes to set them to SKIPPED childNodes = findChildNodes(*workflowExecution, actionResult.Action.ID) // Remove duplicates - log.Printf("CHILD NODES: %d", len(childNodes)) + //log.Printf("CHILD NODES: %d", len(childNodes)) for _, nodeId := range childNodes { if nodeId == actionResult.Action.ID { continue @@ -1290,7 +1290,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } if _, err = tx.Commit(); err != nil { - if attempts >= 5 { + if attempts >= 7 { log.Printf("[ERROR] QUITTING: tx.Commit %d: %v", attempts, err) tx.Rollback() workflowExecution.Status = "ABORTED" @@ -1301,7 +1301,9 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl return } - log.Printf("[WARNING] tx.Commit %d: %v", attempts, err) + if attempts > 3 { + log.Printf("[WARNING] tx.Commit %d: %v", attempts, err) + } attempts += 1 runWorkflowExecutionTransaction(ctx, attempts, workflowExecutionId, actionResult, resp) @@ -2604,7 +2606,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } // This one doesn't really matter. - log.Printf("Running POST execution with data %s", body) + log.Printf("Running POST execution with body of length %d", len(string(body))) var execution ExecutionRequest err = json.Unmarshal(body, &execution) if err != nil { diff --git a/docker-compose.yml b/docker-compose.yml index 7a356dff..c575474a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.42 + image: ghcr.io/frikky/shuffle-frontend:0.8.43 container_name: shuffle-frontend hostname: shuffle-frontend ports: diff --git a/frontend/src/components/OrgHeader.js b/frontend/src/components/OrgHeader.js index 7f21cdce..da6cf847 100644 --- a/frontend/src/components/OrgHeader.js +++ b/frontend/src/components/OrgHeader.js @@ -130,7 +130,7 @@ const OrgHeader = (props) => { return (
- +
0 ? null : "1px solid #f85a3e", cursor: "pointer", backgroundColor: imageData !== undefined && imageData.length > 0 ? null : theme.palette.inputColor, maxWidth: 174, maxHeight: 174}} onClick={() => {upload.click()}}> upload = ref} onChange={editHeaderImage} /> {imageInfo} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index e9f2f1ef..77451c91 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -4366,6 +4366,11 @@ const AngularWorkflow = (props) => { setConditionValue(conditionValue) setVariableAnchorEl(null) }} key={"contains"}>contains + { + conditionValue.value = "contains_any_of" + setConditionValue(conditionValue) + setVariableAnchorEl(null) + }} key={"contains_any_of"}>contains { conditionValue.value = "matches regex" setConditionValue(conditionValue) diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 06a938ff..cf6a8b1a 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -106,7 +106,7 @@ func getThisContainerId() { // cgroup error. Hardcoding this. // https://github.com/moby/moby/issues/7015 - log.Printf("Checking if %s is in %s", ".scope", string(out)) + //log.Printf("Checking if %s is in %s", ".scope", string(out)) if strings.Contains(string(out), ".scope") { containerId = "shuffle-orborus" //docker-76c537e9a4b7c7233011f5d70e6b7f2d600b6413ac58a96519b8dca7a3f7117a.scope diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 067cdbe4..d0267d14 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - //"io" + "io" "io/ioutil" "log" "net/http" @@ -37,7 +37,7 @@ func getThisContainerId() string { if err == nil { id = strings.TrimSpace(string(out)) - log.Printf("Checking if %s is in %s", ".scope", string(out)) + //log.Printf("Checking if %s is in %s", ".scope", string(out)) if strings.Contains(string(out), ".scope") { id = fallbackName } @@ -1370,7 +1370,12 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W log.Printf("Skipping FULL_EXECUTION because size is larger than %d", maxSize) } - // Try original -> Go to lowercase + // Uses a few ways of getting / checking if an app is available + // 1. Try original + // 2. Go to lowercase + // 3. Add remote repo location + // 4. Actually download last repo + err = deployApp(dockercli, image, identifier, env) if err != nil { // Trying to replace with lowercase to deploy again. This seems to work with Dockerhub well. @@ -1389,13 +1394,37 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W err = deployApp(dockercli, image, identifier, env) if err != nil { - - log.Printf("[ERROR] Failed deploying image THRICE. Aborting if the image doesn't exist") - if strings.Contains(err.Error(), "No such image") { - //log.Printf("[WARNING] Failed deploying %s from image %s: %s", identifier, image, err) - log.Printf("[ERROR] Image doesn't exist. Shutting down") + log.Printf("[WARNING] Failed deploying image THRICE. Attempting to download the latter as last resort.") + reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) + if err != nil { + log.Printf("[ERROR] Failed getting %s. The couldn't be find locally, AND is missing.", image) shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) } + + buildBuf := new(strings.Builder) + _, err = io.Copy(buildBuf, reader) + if err != nil { + log.Printf("[ERROR] Error in IO copy: %s", err) + shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + } else { + if strings.Contains(buildBuf.String(), "errorDetail") { + log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), image) + shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + } + + log.Printf("[INFO] Successfully downloaded %s", image) + } + + err = deployApp(dockercli, image, identifier, env) + if err != nil { + + log.Printf("[ERROR] Failed deploying image for the FOURTH time. Aborting if the image doesn't exist") + if strings.Contains(err.Error(), "No such image") { + //log.Printf("[WARNING] Failed deploying %s from image %s: %s", identifier, image, err) + log.Printf("[ERROR] Image doesn't exist. Shutting down") + shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + } + } } } } From 287840b961b0dc015cde54f6d5f4a2efcd259764 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 26 Dec 2020 17:04:40 +0100 Subject: [PATCH 030/185] BUG: Fixed resource overload when getting workflows --- backend/app_sdk/build.sh | 2 +- backend/go-app/docker.go | 6 +-- backend/go-app/main.go | 61 ++++++++++++++------------ backend/go-app/walkoff.go | 25 ++++++++--- docker-compose.yml | 2 +- frontend/src/views/AngularWorkflow.jsx | 4 +- 6 files changed, 59 insertions(+), 41 deletions(-) diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index f96da989..08128a44 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.4 +VERSION=0.8.41 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 03720e3b..5775b537 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -501,7 +501,7 @@ func handleStopHookDocker(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() hook, err := getHook(ctx, fileId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (stop docker): %s", fileId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -633,7 +633,7 @@ func handleStartHookDocker(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() hook, err := getHook(ctx, fileId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (start docker): %s", fileId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -781,7 +781,7 @@ func hookTest() { returnHook, err := getHook(ctx, hook.Id) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (test): %s", hook.Id, err) } if len(returnHook.Id) > 0 { diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 263e0872..d6ebc6cb 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3096,7 +3096,7 @@ func handleSetHook(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() _, err = getHook(ctx, workflowId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (set): %s", workflowId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "message": "Invalid ID"}`)) return @@ -3457,7 +3457,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { //log.Printf("HookID: %s", hookId) hook, err := getHook(ctx, hookId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (callback): %s", hookId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -3470,7 +3470,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { //resp.WriteHeader(200) //resp.Write([]byte(`{"success": true}`)) if hook.Status == "stopped" { - log.Printf("Not running because hook status is stopped") + log.Printf("Not running %s because hook status is stopped", hook.Id) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Click start to start it"}`))) return @@ -3493,20 +3493,34 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { ExecutionArgument string `json:"execution_argument"` } + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("Body data error: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + newBody := ExecutionStruct{ + Start: hook.Start, + ExecutionSource: "webhook", + ExecutionArgument: string(body), + } + + b, err := json.Marshal(newBody) + if err != nil { + log.Printf("Failed newBody marshaling: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + for _, item := range hook.Workflows { log.Printf("Running webhook for workflow %s with startnode %s", item, hook.Start) workflow := Workflow{ ID: "", } - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Body data error: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - //parsedBody := string(body) //parsedBody = strings.Replace(parsedBody, "\"", "\\\"", -1) //if len(parsedBody) > 0 { @@ -3515,20 +3529,6 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // } //} - newBody := ExecutionStruct{ - Start: hook.Start, - ExecutionSource: "webhook", - ExecutionArgument: string(body), - } - - b, err := json.Marshal(newBody) - if err != nil { - log.Printf("Failed newBody marshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - //bodyWrapper := fmt.Sprintf(`{"start": "%s", "execution_source": "webhook", "execution_argument": "%s"}`, hook.Start, string(parsedBody)) //if len(hook.Start) == 0 { // log.Printf("No start node for hook %s - running with workflow default.", hook.Id) @@ -3798,7 +3798,7 @@ func sendHookResult(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() hook, err := getHook(ctx, workflowId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (send): %s", workflowId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -3865,7 +3865,7 @@ func handleGetHook(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() hook, err := getHook(ctx, workflowId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (get hook): %s", workflowId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -7152,7 +7152,12 @@ func runInit(ctx context.Context) { } } } else { - log.Printf("Found %d users.", len(users)) + if len(users) == 1 { + log.Printf("Found 1 user - %s.", users[0].Username) + } else { + log.Printf("Found %d users.", len(users)) + } + if len(activeOrgs) == 1 && len(users) > 0 { for _, user := range users { if user.ActiveOrg.Id == "" && len(user.Username) > 0 { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 85796a74..5dab4903 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1281,8 +1281,11 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Prevents timing issues //ExecutionId if _, err := tx.Put(key, workflowExecution); err != nil { - tx.Rollback() - log.Printf("[ERROR] tx.Put bug: %v", err) + log.Printf("[ERROR] tx.Put error: %v", err) + err = tx.Rollback() + if err != nil { + log.Printf("[ERROR] Rollback error (3): %s", err) + } resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed setting workflowexecution actionresult: %s"}`, err))) @@ -1290,9 +1293,14 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } if _, err = tx.Commit(); err != nil { + err = tx.Rollback() + if err != nil { + log.Printf("[ERROR] Rollback error expected ? (1): %s", err) + } + if attempts >= 7 { log.Printf("[ERROR] QUITTING: tx.Commit %d: %v", attempts, err) - tx.Rollback() + workflowExecution.Status = "ABORTED" setWorkflowExecution(ctx, *workflowExecution) @@ -1308,6 +1316,11 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl attempts += 1 runWorkflowExecutionTransaction(ctx, attempts, workflowExecutionId, actionResult, resp) return + } else { + //if grpc.Code(err) == codes.Aborted { + // return nil, ErrConcurrentTransaction + //} + //t.id = nil // mark the transaction as expired } resp.WriteHeader(200) @@ -6093,7 +6106,7 @@ func handleStopHook(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() hook, err := getHook(ctx, fileId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (stop): %s", fileId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -6176,7 +6189,7 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() hook, err := getHook(ctx, fileId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (delete): %s", fileId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -6317,7 +6330,7 @@ func handleStartHook(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() hook, err := getHook(ctx, fileId) if err != nil { - log.Printf("Failed getting hook: %s", err) + log.Printf("Failed getting hook %s (start): %s", fileId, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return diff --git a/docker-compose.yml b/docker-compose.yml index c575474a..81934cf1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.43 + image: ghcr.io/frikky/shuffle-backend:0.8.44 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 77451c91..84220f25 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -4370,7 +4370,7 @@ const AngularWorkflow = (props) => { conditionValue.value = "contains_any_of" setConditionValue(conditionValue) setVariableAnchorEl(null) - }} key={"contains_any_of"}>contains + }} key={"contains_any_of"}>contains any of { conditionValue.value = "matches regex" setConditionValue(conditionValue) @@ -5979,7 +5979,7 @@ const AngularWorkflow = (props) => { {workflowExecutions.length > 0 ?
{workflowExecutions.map(data => { - const statusColor = data.status === "FINISHED" ? "green" : data.status === "ABORTED" ? "red" : "orange" + const statusColor = data.status === "FINISHED" ? "green" : data.status === "ABORTED" || data.status === "FAILED" ? "red" : "orange" const timeElapsed = data.completed_at-data.started_at const resultsLength = data.results !== undefined && data.results !== null ? data.results.length : 0 From 7f12df6e31575b85e19906c23302842243550525 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 26 Dec 2020 18:34:57 +0100 Subject: [PATCH 031/185] Added copy feature for JSON objects --- backend/go-app/docker.go | 6 ++-- backend/go-app/walkoff.go | 12 +++---- frontend/src/views/AngularWorkflow.jsx | 47 +++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 5775b537..06b9b086 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -202,7 +202,7 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin tw := tar.NewWriter(buf) defer tw.Close() - log.Printf("Setting up memory build structure for folder: %s", dockerfileFolder) + log.Printf("[INFO] Setting up memory build structure for folder: %s", dockerfileFolder) err = getParsedTarMemory(fs, tw, dockerfileFolder, "") if err != nil { log.Printf("Tar issue: %s", err) @@ -230,7 +230,7 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin } // Build the actual image - log.Printf("Building %s. This may take up to a few minutes.", dockerfileFolder) + log.Printf("[INFO] Building %s. This may take up to a few minutes.", dockerfileFolder) imageBuildResponse, err := client.ImageBuild( ctx, dockerFileTarReader, @@ -713,7 +713,7 @@ func handleStartHookDocker(resp http.ResponseWriter, request *http.Request) { } // FIXME - get some real data? - log.Printf("Successfully started %s-%s on port %s with filepath %s", image, fileId, port, filepath) + log.Printf("[INFO] Successfully started %s-%s on port %s with filepath %s", image, fileId, port, filepath) resp.WriteHeader(200) resp.Write([]byte(`{"success": true, "message": "Started webhook"}`)) return diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 5dab4903..60a01b11 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -5776,23 +5776,23 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin log.Printf("Failed image build memory: %s", err) } else { if len(item.Tags) > 0 { - log.Printf("Successfully built image %s", item.Tags[0]) + log.Printf("[INFO] Successfully built image %s", item.Tags[0]) } else { - log.Printf("Successfully built Docker image") + log.Printf("[INFO] Successfully built Docker image") } } } - log.Printf("Starting build of %d skipped docker images", len(buildLaterList)) + log.Printf("[INFO] Starting build of %d skipped docker images", len(buildLaterList)) for _, item := range buildLaterList { err = buildImageMemory(fs, item.Tags, item.Extra) if err != nil { - log.Printf("Failed image build memory: %s", err) + log.Printf("[INFO] Failed image build memory: %s", err) } else { if len(item.Tags) > 0 { - log.Printf("Successfully built image %s", item.Tags[0]) + log.Printf("[INFO] Successfully built image %s", item.Tags[0]) } else { - log.Printf("Successfully built Docker image") + log.Printf("[INFO] Successfully built Docker image") } } } diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 84220f25..e03e4138 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -5957,8 +5957,49 @@ const AngularWorkflow = (props) => { ) } + + const HandleJsonCopy = (base, copy, base_node_name) => { + console.log("COPY: ", copy) + var newitem = JSON.parse(base) + var to_be_copied = "$"+base_node_name + for (var key in copy.namespace) { + if (copy.namespace[key].includes("Results for")) { + continue + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.namespace[key]] + if (!isNaN(copy.namespace[key])) { + to_be_copied += ".#" + } else { + to_be_copied += "."+copy.namespace[key] + } + } + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.name] + if (!isNaN(copy.name)) { + to_be_copied += ".#" + } else { + to_be_copied += "."+copy.name + } + } + + console.log(to_be_copied) + //var copyText = document.getElementById("copy_element_shuffle"); + //if (copyText !== null) { + //navigator.clipboard.writeText(to_be_copied) + //copyText.select(); + //copyText.setSelectionRange(0, 99999); /* For mobile devices */ + + ///* Copy the text inside the text field */ + //document.execCommand("copy"); + //} + } + const executionModal = - setExecutionModalOpen(false)} PaperProps={{style: {minWidth: 375, maxWidth: 375, backgroundColor: "#1F2023", color: "white", fontSize: 18}}}> + setExecutionModalOpen(false)} style={{resize: "both", overflow: "auto",}} PaperProps={{style: {resize: "both", overflow: "auto", minWidth: 400, maxWidth: 400, backgroundColor: "#1F2023", color: "white", fontSize: 18}}}> {executionModalView === 0 ?
@@ -6136,6 +6177,10 @@ const AngularWorkflow = (props) => { theme="solarized" collapsed={true} displayDataTypes={false} + onSelect={(select) => { + HandleJsonCopy(showResult, select, data.action.name) + console.log("SELECTED!: ", select) + }} name={"Results for "+data.action.label} /> : From 49ee75006c5fae727113771e41cab11de1c21b34 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 27 Dec 2020 08:30:22 +0100 Subject: [PATCH 032/185] Feature: added popuut result --- frontend/src/views/AngularWorkflow.jsx | 135 ++++++++++++++++++++++--- frontend/src/views/Workflows.jsx | 85 ++++++++-------- 2 files changed, 166 insertions(+), 54 deletions(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index e03e4138..73382661 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -19,6 +19,7 @@ import Select from '@material-ui/core/Select'; import MenuItem from '@material-ui/core/MenuItem'; import Divider from '@material-ui/core/Divider'; import Dialog from '@material-ui/core/Dialog'; +import Modal from '@material-ui/core/Modal'; import DialogActions from '@material-ui/core/DialogActions'; import DialogTitle from '@material-ui/core/DialogTitle'; import InputLabel from '@material-ui/core/InputLabel'; @@ -37,8 +38,11 @@ import Switch from '@material-ui/core/Switch'; import ReactJson from 'react-json-view' import { useBeforeunload } from 'react-beforeunload'; import NestedMenuItem from "material-ui-nested-menu-item"; +import Fade from '@material-ui/core/Fade'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; +import CloseIcon from '@material-ui/icons/Close'; +import ArrowLeftIcon from '@material-ui/icons/ArrowLeft'; import CachedIcon from '@material-ui/icons/Cached'; import AddIcon from '@material-ui/icons/Add'; import DirectionsRunIcon from '@material-ui/icons/DirectionsRun'; @@ -75,6 +79,7 @@ import cxtmenu from 'cytoscape-cxtmenu'; import { w3cwebsocket as W3CWebSocket } from "websocket"; import { useAlert } from "react-alert"; +import { validateJson } from "./Workflows"; import { GetParsedPaths } from "./Apps"; const surfaceColor = "#27292D" @@ -112,6 +117,7 @@ const AngularWorkflow = (props) => { const [bodyWidth, bodyHeight] = useWindowSize(); const appBarSize = 74 + var to_be_copied = "" const [cystyle, ] = useState(cytoscapestyle) const [cy, setCy] = React.useState() @@ -141,6 +147,9 @@ const AngularWorkflow = (props) => { const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false) const [showSkippedActions, setShowSkippedActions] = React.useState(false) + const [selectedResult, setSelectedResult] = React.useState({}) + const [codeModalOpen, setCodeModalOpen] = React.useState(false); + const [variableAnchorEl, setVariableAnchorEl] = React.useState(null) const [sourceValue, setSourceValue] = React.useState({}) @@ -5957,11 +5966,10 @@ const AngularWorkflow = (props) => { ) } - const HandleJsonCopy = (base, copy, base_node_name) => { console.log("COPY: ", copy) var newitem = JSON.parse(base) - var to_be_copied = "$"+base_node_name + to_be_copied = "$"+base_node_name for (var key in copy.namespace) { if (copy.namespace[key].includes("Results for")) { continue @@ -5986,16 +5994,18 @@ const AngularWorkflow = (props) => { } } - console.log(to_be_copied) - //var copyText = document.getElementById("copy_element_shuffle"); - //if (copyText !== null) { - //navigator.clipboard.writeText(to_be_copied) - //copyText.select(); - //copyText.setSelectionRange(0, 99999); /* For mobile devices */ + to_be_copied.replace(" ", "_") + var copyText = document.getElementById("copy_element_shuffle"); + if (copyText !== null) { + navigator.clipboard.writeText(to_be_copied) + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ - ///* Copy the text inside the text field */ - //document.execCommand("copy"); - //} + /* Copy the text inside the text field */ + document.execCommand("copy"); + } + + alert.success("Copied "+to_be_copied) } const executionModal = @@ -6124,6 +6134,7 @@ const AngularWorkflow = (props) => { if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED" || data.status === "FAILURE")) { return null } + console.log(data) // showResult = replaceAll(showResult, " None", " \"None\"") @@ -6165,6 +6176,14 @@ const AngularWorkflow = (props) => { return (
+ { + setSelectedResult(data) + setCodeModalOpen(true) + }}> + + + + {actionimg}
{data.action.label}
@@ -6178,7 +6197,7 @@ const AngularWorkflow = (props) => { collapsed={true} displayDataTypes={false} onSelect={(select) => { - HandleJsonCopy(showResult, select, data.action.name) + HandleJsonCopy(showResult, select, data.action.label) console.log("SELECTED!: ", select) }} name={"Results for "+data.action.label} @@ -6620,6 +6639,89 @@ const AngularWorkflow = (props) => { ) } + const CodePopoutModal = (props) => { + const {codeModalOpen, setCodeModalOpen, selectedResult} = props + if (!codeModalOpen) { + return null + } + + const curapp = apps.find(a => a.name === selectedResult.action.app_name && a.app_version === selectedResult.action.app_version) + const imgsize = 50 + const statusColor = selectedResult.status === "FINISHED" || selectedResult.status === "SUCCESS" ? "green" : selectedResult.status === "ABORTED" || selectedResult.status === "FAILURE" ? "red" : "orange" + const validate = validateJson(selectedResult.result.trim()) + + return ( + + { + //setCodeModalOpen(false) + }} + PaperComponent= + BackdropProps={{ + invisible: false, + open: false, + style: { + height: 0, + width: 0, + padding: 0, + margin: 0, + background: "none", + boxShadow: "none", + backgroundColor: "transparent", + } + }} + PaperProps={{ + style: { + backgroundColor: surfaceColor, + color: "white", + minWidth: 750, + padding: 30, + maxHeight: 700, + overflow: "auto", + //backgroundColor: "transparent", + boxShadow: "none", + }, + }} + > + { + setCodeModalOpen(false) + }}> + + +
+
+ {curapp === null ? null : {selectedResult.app_name}} +
+
{selectedResult.action.label}
+
{selectedResult.action.name}
+
+
+
Status {selectedResult.status}
+ {validate.valid ? { + HandleJsonCopy(JSON.stringify(validate.result), select, selectedResult.action.label) + }} + name={"Results for "+selectedResult.action.label} + /> + : +
+ Result  + {selectedResult.result} +
+ } +
+
+
+ ) + } + // This whole part is redundant. Made it part of Arguments instead. const authenticationModal = authenticationModalOpen ? { {executionVariableModal} {conditionsModal} {authenticationModal} + + {/* + + */}
:
diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 3b0ec8bc..6c9ead3b 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -43,6 +43,37 @@ import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; const inputColor = "#383B40" const surfaceColor = "#27292D" +export const validateJson = (showResult) => { + showResult = showResult.split(" None").join(" \"None\"") + showResult = showResult.split(" False").join(" false") + showResult = showResult.split(" True").join(" true") + + var jsonvalid = true + try { + const tmp = String(JSON.parse(showResult)) + if (!showResult.includes("{") && !showResult.includes("[")) { + jsonvalid = false + } + } catch (e) { + showResult = showResult.split("\'").join("\"") + + try { + const tmp = String(JSON.parse(showResult)) + if (!showResult.includes("{") && !showResult.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } + } + + console.log("VALID: ", jsonvalid) + return { + "valid": jsonvalid, + "result": jsonvalid ? JSON.parse(showResult) : showResult, + } +} + const Workflows = (props) => { const { globalUrl, isLoggedIn, isLoaded, removeCookie, cookies, userdata} = props; document.title = "Shuffle - Workflows" @@ -684,22 +715,12 @@ const Workflows = (props) => { } var t = new Date(data.started_at*1000) - var jsonvalid = true var showResult = data.result.trim() - showResult = replaceAll(showResult, " None", " \"None\""); - try { - const tmp = String(JSON.parse(showResult)) - if (!tmp.includes("{") && !tmp.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } + const validate = validateJson(showResult) - //console.log("VALID: ", jsonvalid) - if (jsonvalid) { + if (validate.valid) { showResult = { /> } else { // FIXME - have everything parsed as json, either just for frontend - // or in the backend + // or in the backend? /* const newdata = {"result": data.result} showResult = { No results yet
- const resultsLength = Object.getOwnPropertyNames(selectedExecution).length > 0 && selectedExecution.results !== null ? selectedExecution.results.length : 0 + const resultsLength = Object.getOwnPropertyNames(selectedExecution).length > 0 && selectedExecution.results !== null ? selectedExecution.results.length : 0 const ExecutionDetails = () => { var starttime = new Date(selectedExecution.started_at*1000) @@ -780,23 +801,12 @@ const Workflows = (props) => { var arg = null if (selectedExecution.execution_argument !== undefined && selectedExecution.execution_argument.length > 0) { - var jsonvalid = true - var showResult = selectedExecution.execution_argument.trim() - showResult = replaceAll(showResult, " None", " \"None\""); + const validate = validateJson(showResult) - try { - const tmp = String(JSON.parse(showResult)) - if (!tmp.includes("{") && !tmp.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - - arg = jsonvalid ? + arg = validate.valid ? { var lastresult = null if (selectedExecution.result !== undefined && selectedExecution.result.length > 0) { - var jsonvalid = true var showResult = selectedExecution.result.trim() - showResult = replaceAll(showResult, " None", " \"None\""); + const validate = validateJson(showResult) + console.log("VALID: ", validate) - try { - const tmp = JSON.parse(showResult) - if (!tmp.includes("{") && !tmp.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - - lastresult = jsonvalid ? + lastresult = validate.valid ? Date: Sun, 27 Dec 2020 08:57:06 +0100 Subject: [PATCH 033/185] Got modal with Draggable working --- frontend/src/views/AngularWorkflow.jsx | 133 ++++++++++++------------- frontend/src/views/Workflows.jsx | 1 - 2 files changed, 65 insertions(+), 69 deletions(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 73382661..3a3598bc 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -6005,7 +6005,7 @@ const AngularWorkflow = (props) => { document.execCommand("copy"); } - alert.success("Copied "+to_be_copied) + //alert.success("Copied "+to_be_copied) } const executionModal = @@ -6134,8 +6134,6 @@ const AngularWorkflow = (props) => { if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED" || data.status === "FAILURE")) { return null } - console.log(data) - // showResult = replaceAll(showResult, " None", " \"None\"") // Super basic check. @@ -6651,74 +6649,73 @@ const AngularWorkflow = (props) => { const validate = validateJson(selectedResult.result.trim()) return ( - - { - //setCodeModalOpen(false) - }} - PaperComponent= - BackdropProps={{ - invisible: false, - open: false, - style: { - height: 0, - width: 0, - padding: 0, - margin: 0, - background: "none", - boxShadow: "none", - backgroundColor: "transparent", - } - }} - PaperProps={{ - style: { - backgroundColor: surfaceColor, - color: "white", - minWidth: 750, - padding: 30, - maxHeight: 700, - overflow: "auto", - //backgroundColor: "transparent", - boxShadow: "none", - }, - }} - > - { - setCodeModalOpen(false) - }}> - - -
-
- {curapp === null ? null : {selectedResult.app_name}} -
-
{selectedResult.action.label}
-
{selectedResult.action.name}
-
-
-
Status {selectedResult.status}
- {validate.valid ? { - HandleJsonCopy(JSON.stringify(validate.result), select, selectedResult.action.label) - }} - name={"Results for "+selectedResult.action.label} - /> - : + + { + //setCodeModalOpen(false) + }} + BackdropProps={{ + invisible: true, + style: { + backgroundColor: "transparent", + pointerEvents: "none", + } + }} + PaperProps={{ + style: { + pointerEvents: "auto", + backgroundColor: inputColor, + color: "white", + minWidth: 750, + padding: 30, + maxHeight: 700, + overflow: "auto", + //boxShadow: "none", + }, + }} + > + { + setCodeModalOpen(false) + }}> + + +
+
+ {curapp === null ? null : {selectedResult.app_name}}
- Result  - {selectedResult.result} +
{selectedResult.action.label}
+
{selectedResult.action.name}
- }
-
-
+
Status {selectedResult.status}
+ {validate.valid ? { + HandleJsonCopy(JSON.stringify(validate.result), select, selectedResult.action.label) + }} + name={"Results for "+selectedResult.action.label} + /> + : +
+ Result  + {selectedResult.result} +
+ } +
+
+
) } diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 6c9ead3b..57379cad 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -67,7 +67,6 @@ export const validateJson = (showResult) => { } } - console.log("VALID: ", jsonvalid) return { "valid": jsonvalid, "result": jsonvalid ? JSON.parse(showResult) : showResult, From 697dcb885e6f42465279eb158aab6592cbc33b9b Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 29 Dec 2020 15:43:07 +0100 Subject: [PATCH 034/185] Reduced API-calls to backend --- backend/app_sdk/app_base.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 98f62feb..155a09d7 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -457,11 +457,13 @@ class AppBase: action_result["result"] = "Error in setup ENV: ACTION not defined" self.send_result(action_result, headers, stream_path) return + if len(self.authorization) == 0: print("AUTHORIZATION env not defined") action_result["result"] = "Error in setup ENV: AUTHORIZATION not defined" self.send_result(action_result, headers, stream_path) return + if len(self.current_execution_id) == 0: print("EXECUTIONID env not defined") action_result["result"] = "Error in setup ENV: EXECUTIONID not defined" @@ -476,17 +478,19 @@ class AppBase: # Add async logger # self.console_logger.handlers[0].stream.set_execution_id() #self.logger.info("Before initial stream result") - try: - ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result) - self.logger.info("Workflow: %d" % ret.status_code) - if ret.status_code != 200: - self.logger.info(ret.text) - except requests.exceptions.ConnectionError as e: - print("Connectionerror: %s" % e) - action_result["result"] = "Bad setup during startup: %s" % e - self.send_result(action_result, headers, stream_path) - return + # FIXME: Shouldn't skip this, but it's good for minimzing API calls + #try: + # ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result) + # self.logger.info("Workflow: %d" % ret.status_code) + # if ret.status_code != 200: + # self.logger.info(ret.text) + #except requests.exceptions.ConnectionError as e: + # print("Connectionerror: %s" % e) + + # action_result["result"] = "Bad setup during startup: %s" % e + # self.send_result(action_result, headers, stream_path) + # return # Verify whether there are any parameters with ACTION_RESULT required # If found, we get the full results list from backend From 5beeadc1b1151e8b79b4c4fdfadf169acc9698cf Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 29 Dec 2020 16:26:40 +0100 Subject: [PATCH 035/185] FEATURE: Added popout results in workflows --- backend/app_sdk/build.sh | 2 +- frontend/src/views/AngularWorkflow.jsx | 279 ++++++++++++++++--------- frontend/src/views/Workflows.jsx | 2 - 3 files changed, 178 insertions(+), 105 deletions(-) diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 08128a44..67c75fd7 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.41 +VERSION=0.8.42 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 3a3598bc..a2adc14d 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -155,6 +155,11 @@ const AngularWorkflow = (props) => { const [sourceValue, setSourceValue] = React.useState({}) const [destinationValue, setDestinationValue] = React.useState({}) const [conditionValue, setConditionValue] = React.useState({}) + const [dragging, setDragging] = React.useState(false) + const [dragPosition, setDragPosition] = React.useState({ + x: 0, + y: 0, + }) // Trigger stuff const [selectedTrigger, setSelectedTrigger] = React.useState({}); @@ -1267,13 +1272,13 @@ const AngularWorkflow = (props) => { // CTRL = 17 //console.log(event.keyCode) switch( event.keyCode ) { - case 27: - console.log("ESCAPE") - break; + case 27: + console.log("ESCAPE") + break; case 46: removeNode() console.log("DELETE") - break; + break; case 38: console.log("UP") break; @@ -1313,11 +1318,11 @@ const AngularWorkflow = (props) => { } break; case 70: - if (previouskey === 17) { - event.preventDefault() - cy.fit(null, 50) - } - break; + //if (previouskey === 17) { + // event.preventDefault() + // cy.fit(null, 50) + //} + //break; case 65: // As a poweruser myself, I found myself hitting this a few // too many times to just edit text. Need a better bind @@ -3676,13 +3681,42 @@ const AngularWorkflow = (props) => {

{selectedAction.app_name}

- What are actions? - {selectedAction.errors !== null && selectedAction.errors.length > 0 ? -
- Errors: {selectedAction.errors.join("\n")} -
- : null - } +
+ { + console.log("FIND EXAMPLE RESULTS FOR ", selectedAction) + if (workflowExecutions.length > 0) { + // Look for the ID + const found = false + for (var key in workflowExecutions) { + if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { + continue + } + + var foundResult = workflowExecutions[key].results.find(result => result.action.id === selectedAction.id) + if (foundResult === undefined || foundResult === null) { + continue + } + + setSelectedResult(foundResult) + setCodeModalOpen(true) + break + } + } + }}> + + + + + + What are actions? + {selectedAction.errors !== null && selectedAction.errors.length > 0 ? +
+ Errors: {selectedAction.errors.join("\n")} +
+ : null + } +
+
+ + Conditions can't be used for loops [ .# ]. Learn more + - - + setSelectedEdge(selectedEdge) + workflow.branches[selectedEdgeIndex] = selectedEdge + setWorkflow(workflow) + }} color="primary"> + Submit + + @@ -6422,15 +6436,20 @@ const AngularWorkflow = (props) => { :
-

{ - setExecutionRunning(false) - stop() - getWorkflowExecution(props.match.params.key) - setExecutionModalView(0) + { + setExecutionRunning(false) + stop() + getWorkflowExecution(props.match.params.key) + setExecutionModalView(0) }}> - - See other Executions -

+ {}}> + + +

{ + }}> + See other Executions +

+

Executing Workflow

diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index c9312c6d..97689cac 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -341,7 +341,7 @@ const Apps = (props) => { } var description = data.description - const maxDescLen = 60 + const maxDescLen = 58 if (description.length > maxDescLen) { description = data.description.slice(0, maxDescLen)+"..." } diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 7a29fcfa..5fdb61ea 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.52 +VERSION=0.8.53 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 42e9d097..5a7c0285 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -49,6 +49,8 @@ var baseUrl = os.Getenv("BASE_URL") var environment = os.Getenv("ENVIRONMENT_NAME") var dockerApiVersion = os.Getenv("DOCKER_API_VERSION") var runningMode = strings.ToLower(os.Getenv("RUNNING_MODE")) +var cleanupEnv = strings.ToLower(os.Getenv("CLEANUP")) +var workerIds = []string{} type ExecutionRequestWrapper struct { Data []ExecutionRequest `json:"data"` @@ -144,6 +146,10 @@ func deployWorker(image string, identifier string, env []string) { //log.Printf("[INFO] Empty self container id, continue without NetworkMode") } + if cleanupEnv == "true" { + hostConfig.AutoRemove = true + } + config := &container.Config{ Image: image, Env: env, @@ -212,6 +218,7 @@ func deployWorker(image string, identifier string, env []string) { //} } else { log.Printf("[INFO] Container %s was created under environment %s", cont.ID, environment) + //workerIds = append(workerIds, cont.ID) } return @@ -248,7 +255,7 @@ func initializeImages() { log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.8.52" + workerVersion = "0.8.53" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } @@ -499,6 +506,7 @@ func main() { fmt.Sprintf("EXECUTIONID=%s", execution.ExecutionId), fmt.Sprintf("ENVIRONMENT_NAME=%s", environment), fmt.Sprintf("BASE_URL=%s", baseUrl), + fmt.Sprintf("CLEANUP=%s", cleanupEnv), } if strings.ToLower(os.Getenv("SHUFFLE_PASS_WORKER_PROXY")) != "false" { @@ -512,7 +520,7 @@ func main() { go deployWorker(workerImage, containerName, env) - log.Printf("[INFO] %s was deployed and to be removed from queue.", execution.ExecutionId) + log.Printf("[INFO] ExecutionID %s was deployed and to be removed from queue.", execution.ExecutionId) zombiecounter += 1 toBeRemoved.Data = append(toBeRemoved.Data, execution) } diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 29828da4..fac6304e 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-worker -VERSION=0.8.52 +VERSION=0.8.53 echo "Running docker build with $NAME:$VERSION" #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . @@ -10,5 +10,5 @@ docker build . -t frikky/shuffle:$NAME -t frikky/shuffle:$NAME_$VERSION -t docke #docker push frikky/shuffle:$NAME_$VERSION #docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION #docker tag frikky/shuffle:0.8.51 ghcr.io/frikky/shuffle-worker:0.8.5 -#docker push ghcr.io/frikky/$NAME:$VERSION docker tag frikky/shuffle:$NAME ghcr.io/frikky/shuffle-worker:0.8.52 +docker push ghcr.io/frikky/$NAME:$VERSION diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 49c4b583..016ec96f 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -28,6 +28,7 @@ import ( var environment = os.Getenv("ENVIRONMENT_NAME") var baseUrl = os.Getenv("BASE_URL") var appCallbackUrl = os.Getenv("BASE_URL") +var cleanupEnv = strings.ToLower(os.Getenv("CLEANUP")) var baseimagename = "frikky/shuffle" var registryName = "registry.hub.docker.com" var fallbackName = "shuffle-orborus" @@ -43,6 +44,7 @@ var children map[string][]string var visited []string var executed []string var nextActions []string +var containerIds []string var extra int var startAction string @@ -68,9 +70,9 @@ func getThisContainerId() string { func init() { containerId = getThisContainerId() if len(containerId) == 0 { - log.Printf("[ERROR] No container ID found. Not running containerized?") + log.Printf("[WARNING] No container ID found. Not running containerized? This should only show during testing") } else { - log.Printf("[INFO] Found container ID: %s", containerId) + log.Printf("[INFO] Found container ID for this worker: %s", containerId) } } @@ -770,35 +772,28 @@ type AppExecutionExample struct { // removes every container except itself (worker) func shutdown(executionId, workflowId string) { - dockercli, err := dockerclient.NewEnvClient() - if err != nil { - log.Printf("[ERROR] Unable to create docker client: %s", err) - os.Exit(3) - } + log.Printf("[INFO] Shutdown started") - containerOptions := types.ContainerListOptions{ - All: true, - } + // Might not be necessary because of cleanupEnv hostconfig autoremoval + if cleanupEnv == "true" && len(containerIds) > 0 { + ctx := context.Background() + dockercli, err := dockerclient.NewEnvClient() + if err == nil { + log.Printf("[INFO] Cleaning up %d containers", len(containerIds)) + removeOptions := types.ContainerRemoveOptions{ + RemoveVolumes: true, + Force: true, + } - containers, err := dockercli.ContainerList(context.Background(), containerOptions) - if err != nil { - panic(err) - } - _ = containers - - for _, container := range containers { - for _, name := range container.Names { - if strings.Contains(name, executionId) { - // FIXME - reinstate - not here for debugging - //err = removeContainer(container.ID) - //if err != nil { - // log.Printf("Failed removing %s before shutdown.", name) - //} - - break + for _, containername := range containerIds { + log.Printf("[INFO] Stopping and removing container %s", containername) + dockercli.ContainerStop(ctx, containername, nil) + dockercli.ContainerRemove(ctx, containername, removeOptions) + //removeContainers = append(removeContainers, containername) } } - + } else { + log.Printf("[INFO] NOT cleaning up containers. IDS: %d, CLEANUP env: %s", len(containerIds), cleanupEnv) } fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/executions/%s/abort", baseUrl, workflowId, executionId) @@ -844,7 +839,9 @@ func shutdown(executionId, workflowId string) { log.Printf("[INFO] Failed abort request: %s", err) } - log.Printf("[INFO] Finished shutdown.") + log.Printf("[INFO] Finished shutdown (after 15 seconds).") + // Allows everything to finish in subprocesses + time.Sleep(time.Duration(15) * time.Second) os.Exit(3) } @@ -865,6 +862,10 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] log.Printf("[WARNING] Empty self container id, continue without NetworkMode") } + if cleanupEnv == "true" { + hostConfig.AutoRemove = true + } + config := &container.Config{ Image: image, Env: env, @@ -892,6 +893,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] } log.Printf("[INFO] Container %s is created for %s", cont.ID, identifier) + containerIds = append(containerIds, cont.ID) return nil } @@ -1537,71 +1539,16 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // FIXME - new request here // FIXME - clean up stopped (remove) containers with this execution id - dockercli, err := dockerclient.NewEnvClient() - if err != nil { - log.Printf("Unable to create docker client: %s", err) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) - } if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions)+extra { shutdownCheck := true - ctx := context.Background() for _, result := range workflowExecution.Results { if result.Status == "EXECUTING" { // Cleaning up executing stuff shutdownCheck = false - // Check status - containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{ - All: true, - }) - if err != nil { - log.Printf("Failed listing containers: %s", err) - continue - } - - stopContainers := []string{} - removeContainers := []string{} - for _, container := range containers { - for _, name := range container.Names { - if !strings.Contains(name, result.Action.ID) { - continue - } - - if container.State != "running" { - removeContainers = append(removeContainers, container.ID) - stopContainers = append(stopContainers, container.ID) - } - } - } - - // FIXME - add killing of apps with same execution ID too - // FIXME - stahp - //for _, containername := range stopContainers { - // if err := dockercli.ContainerStop(ctx, containername, nil); err != nil { - // log.Printf("Unable to stop container: %s", err) - // } else { - // log.Printf("Stopped container %s", containername) - // } - //} - - removeOptions := types.ContainerRemoveOptions{ - RemoveVolumes: true, - Force: true, - } - - _ = removeOptions - - // FIXME - this - //for _, containername := range removeContainers { - // if err := dockercli.ContainerRemove(ctx, containername, removeOptions); err != nil { - // log.Printf("Unable to remove container: %s", err) - // } else { - // log.Printf("Removed container %s", containername) - // } - //} - + // USED TO BE CONTAINER REMOVAL // FIXME - send POST request to kill the container - log.Printf("Should remove (POST request) stopped containers") + //log.Printf("Should remove (POST request) stopped containers") //ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result) } } @@ -2511,7 +2458,7 @@ func validateFinished(workflowExecution WorkflowExecution) { if err != nil { log.Printf("[ERROR] Failed reading body: %s", err) } else { - log.Printf("NEWRESP: %s", string(body)) + log.Printf("[INFO] NEWRESP (from backend): %s", string(body)) } } } @@ -2583,7 +2530,7 @@ func setWorkflowExecution(ctx context.Context, workflowExecution WorkflowExecuti } // GetLocalIP returns the non loopback local IP of the host -func GetLocalIP() string { +func getLocalIP() string { addrs, err := net.InterfaceAddrs() if err != nil { return "" @@ -2599,22 +2546,46 @@ func GetLocalIP() string { return "" } -func webserverSetup() { - hostname := GetLocalIP() +func getAvailablePort() (net.Listener, error) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + log.Printf("[WARNING] Failed to assign port by default. Defaulting to 5001") + //return ":5001" + return nil, err + } - log.Printf("\nStarting webserver on port 5001 with hostname: %s\n", hostname) - log.Printf("OLD HOSTNAME: %s", appCallbackUrl) - appCallbackUrl = fmt.Sprintf("http://%s:5001", hostname) - log.Printf("NEW HOSTNAME: %s", appCallbackUrl) + return listener, nil + //return fmt.Sprintf(":%d", port) } -func runWebserver() { +func webserverSetup(workflowExecution WorkflowExecution) net.Listener { + hostname := getLocalIP() + + // FIXME: This MAY not work because of speed between first + // container being launched and port being assigned to webserver + listener, err := getAvailablePort() + if err != nil { + log.Printf("Failed to created listener: %s", err) + shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + } + port := listener.Addr().(*net.TCPAddr).Port + + log.Printf("\n\nStarting webserver on port %d with hostname: %s\n\n", port, hostname) + log.Printf("OLD HOSTNAME: %s", appCallbackUrl) + appCallbackUrl = fmt.Sprintf("http://%s:%d", hostname, port) + log.Printf("NEW HOSTNAME: %s", appCallbackUrl) + + return listener +} + +func runWebserver(listener net.Listener) { r := mux.NewRouter() r.HandleFunc("/api/v1/streams", handleWorkflowQueue).Methods("POST") r.HandleFunc("/api/v1/streams/results", handleGetStreamResults).Methods("POST", "OPTIONS") http.Handle("/", r) - log.Fatal(http.ListenAndServe(":5001", nil)) + //log.Fatal(http.ListenAndServe(port, nil)) + log.Fatal(http.Serve(listener, nil)) } // Initial loop etc @@ -2735,8 +2706,8 @@ func main() { } log.Printf("Environments: %s. 1 = webserver, 0 or >1 = default", environments) - if len(environments) == 1 { - webserverSetup() + if len(environments) == 1 { //&& len(workflowExecution.Actions)+len(workflowExecution.Triggers) > 1 { + listener := webserverSetup(workflowExecution) err := executionInit(workflowExecution) if err != nil { log.Printf("[INFO] Workflow setup failed: %s", workflowExecution.ExecutionId, err) @@ -2748,7 +2719,7 @@ func main() { handleExecutionResult(workflowExecution) }() - runWebserver() + runWebserver(listener) //log.Printf("Before wait") //wg := sync.WaitGroup{} //wg.Add(1) From 38b454bc38a5f8ccc8a8253c49022bf08bd3fab4 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 17 Jan 2021 15:38:58 +0100 Subject: [PATCH 058/185] Reduced execution bytesize --- backend/go-app/main.go | 2 +- backend/go-app/walkoff.go | 12 +++++++ docker-compose.yml | 4 +-- frontend/src/views/AngularWorkflow.jsx | 29 +++++++++++++---- frontend/src/views/Workflows.jsx | 1 + functions/extensions/aws-lambda/deploy.sh | 9 ++++++ functions/onprem/orborus/orborus.go | 39 +++++++++++++++++++++++ functions/onprem/worker/worker.go | 14 ++++---- 8 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 functions/extensions/aws-lambda/deploy.sh diff --git a/backend/go-app/main.go b/backend/go-app/main.go index c3987846..ef55cead 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3535,7 +3535,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } for _, item := range hook.Workflows { - log.Printf("Running webhook for workflow %s with startnode %s", item, hook.Start) + //log.Printf("Running webhook for workflow %s with startnode %s", item, hook.Start) workflow := Workflow{ ID: "", } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 0ef011a8..df426463 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1727,6 +1727,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { action.IsValid = true } + action.LargeImage = "" newActions = append(newActions, action) } @@ -2797,7 +2798,18 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if len(workflow.Actions) == 0 { workflow.Actions = []Action{} + } else { + newactions := []Action{} + for _, action := range workflow.Actions { + action.LargeImage = "" + action.SmallImage = "" + newactions = append(newactions, action) + log.Printf("ACTION: %#v", action) + } + + workflow.Actions = newactions } + if len(workflow.Branches) == 0 { workflow.Branches = []Branch{} } diff --git a/docker-compose.yml b/docker-compose.yml index 1846975b..2f3172f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: '3' services: frontend: - build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.52 + #build: ./frontend + image: ghcr.io/frikky/shuffle-frontend:0.8.51 container_name: shuffle-frontend hostname: shuffle-frontend ports: diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 2f0b127a..56279e64 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -4479,10 +4479,14 @@ const AngularWorkflow = (props) => { setDestinationValue({}) }} > + + Conditions can't be used for loops [ .# ] Learn more + Condition +
- - Conditions can't be used for loops [ .# ]. Learn more - var imageData = file.length > 0 ? file : fileBase64 diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index b809cf62..ae7ec058 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -31,6 +31,8 @@ import { useTheme } from '@material-ui/core/styles'; import HandlePayment from './HandlePayment' import OrgHeader from '../components/OrgHeader' +import EditIcon from '@material-ui/icons/Edit'; +import SelectAllIcon from '@material-ui/icons/SelectAll'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; import DescriptionIcon from '@material-ui/icons/Description'; @@ -87,6 +89,7 @@ const Admin = (props) => { const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false) const [selectedAuthentication, setSelectedAuthentication] = React.useState({}) const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false) + const [authenticationFields, setAuthenticationFields] = React.useState([]) const [showArchived, setShowArchived] = React.useState(false) const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" @@ -276,9 +279,69 @@ const Admin = (props) => { }) } - - - + const saveAuthentication = (authentication) => { + const data = authentication + const url = globalUrl + '/api/v1/apps/authentication'; + + fetch(url, { + mode: 'cors', + method: 'PUT', + body: JSON.stringify(data), + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed changing authentication") + } else { + //alert.success("Successfully password!") + setSelectedUserModalOpen(false) + } + }), + ) + .catch(error => { + alert.error("Err: " + error.toString()) + }); + } + + const editAuthenticationConfig = (id) => { + const data = { + "id": id, + "action": "assign_everywhere", + } + const url = globalUrl + '/api/v1/apps/authentication/'+id+"/config"; + + fetch(url, { + mode: 'cors', + method: 'POST', + body: JSON.stringify(data), + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed overwriting appauth in workflows") + } else { + alert.success("Successfully updated auth everywhere!") + setSelectedUserModalOpen(false) + getAppAuthentication() + } + }), + ) + .catch(error => { + alert.error("Err: " + error.toString()) + }); + } const onPasswordChange = () => { const data = { "username": selectedUser.username, "newpassword": newPassword } @@ -300,7 +363,7 @@ const Admin = (props) => { if (responseJson["success"] === false) { alert.error("Failed setting new password") } else { - alert.success("Successfully password!") + alert.success("Successfully updated password!") setSelectedUserModalOpen(false) } }), @@ -599,7 +662,7 @@ const Admin = (props) => { return response.json() }) .then((responseJson) => { - console.log(responseJson) + //console.log(responseJson) setFiles(responseJson) }) .catch(error => { @@ -966,8 +1029,8 @@ const Admin = (props) => { }); } - const editAuthenticationModal = - { setSelectedAuthenticationModalOpen(false) }} PaperProps={{ @@ -979,56 +1042,71 @@ const Admin = (props) => { }, }} > - Edit authentication + Edit authentication for {selectedAuthentication.app.name} ({selectedAuthentication.label}) -
- setNewPassword(e.target.value)} - /> - -
- - - + {selectedAuthentication.fields.map((data, index) => { + return ( +
+ {data.key} + { + authenticationFields[index].value = e.target.value + setAuthenticationFields(authenticationFields) + }} + /> +
+ ) + })}
+ + + +
+ : null const editUserModal = { }, }} > - Edit user +
{
: null + const updateAppAuthentication = (field) => { + setSelectedAuthenticationModalOpen(true) + setSelectedAuthentication(field) + //{selectedAuthentication.fields.map((data, index) => { + var newfields = [] + for (var key in field.fields) { + newfields.push({ + "key": field.fields[key].key, + "value": "", + }) + } + setAuthenticationFields(newfields) + } + const authenticationView = curTab === 2 ?
@@ -1952,7 +2044,7 @@ const Admin = (props) => { /> { /> { /> { /> { @@ -2009,16 +2101,28 @@ const Admin = (props) => { style={{minWidth: 200, maxWidth: 200, overflow: "hidden"}} /> - + + ) From 777d8f40c393b914a1892e5ae427012ee7e0f2a8 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 17 Jan 2021 18:32:55 +0100 Subject: [PATCH 062/185] Added previouslysaved fix --- backend/go-app/walkoff.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 5be7cad8..edc1858d 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2239,6 +2239,8 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } if len(outerapp.ID) > 0 && outerapp.Authentication.Required { + // FIXME: Add app auth + //log.Printf("FOUND APP TO VALIDATE: %#v", outerapp) action.Errors = append(action.Errors, "Requires authentication") action.IsValid = false @@ -2257,7 +2259,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { newActions = actionFixing } - //workflow.PreviouslySaved = true + workflow.PreviouslySaved = true } //PreviouslySaved bool `json:"first_save" datastore:"first_save"` From 82f30a38d2e5232a74ab7ebc0a9e48b0c80c73b4 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 17 Jan 2021 19:08:14 +0100 Subject: [PATCH 063/185] #168: Added dummy auths to make it simpler to import new workflows --- backend/go-app/main.go | 6 +- backend/go-app/walkoff.go | 52 +- docker-compose.yml | 6 +- frontend/package-lock.json | 1043 ++++++++++++++++++++++++++++++++---- frontend/package.json | 8 +- 5 files changed, 983 insertions(+), 132 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index fde61383..6e304dd2 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -7536,16 +7536,16 @@ func runInit(ctx context.Context) { if err != nil { log.Printf("Failed to upload workflows from github: %s", err) } else { - log.Printf("Finished downloading workflows from github!") + log.Printf("[INFO] Finished downloading workflows from github!") } } else { - log.Printf("Skipping because there are %d workflows already", len(workflows)) + log.Printf("[INFO] Skipping because there are %d workflows already", len(workflows)) } } } - log.Printf("Finished INIT") + log.Printf("[INFO] Finished INIT") } func handleVerifyCloudsync(orgId string) (SyncFeatures, error) { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index edc1858d..0c5e2fc4 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -140,6 +140,7 @@ type AppAuthenticationStorage struct { OrgId string `json:"org_id" datastore:"org_id"` Created int64 `json:"created" datastore:"created"` Edited int64 `json:"edited" datastore:"edited"` + Defined bool `json:"defined" datastore:"defined"` } type AuthenticationUsage struct { @@ -2220,6 +2221,10 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { continue } + if !auth.Defined { + continue + } + if auth.App.Name == action.AppName { //log.Printf("FOUND AUTH FOR APP %s: %s", auth.App.Name, auth.Id) action.AuthenticationId = auth.Id @@ -2228,6 +2233,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } + // FIXME: Only o this IF there isn't another one for the app already if !authSet { //log.Printf("Validate if the app NEEDS auth or not") outerapp := WorkflowApp{} @@ -2239,9 +2245,45 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } if len(outerapp.ID) > 0 && outerapp.Authentication.Required { - // FIXME: Add app auth + found := false + for _, auth := range allAuths { + if auth.App.ID == outerapp.ID { + found = true + break + } + } + + // FIXME: Add app auth + if !found { + timeNow := int64(time.Now().Unix()) + authFields := []AuthenticationStore{} + for _, param := range outerapp.Authentication.Parameters { + authFields = append(authFields, AuthenticationStore{ + Key: param.Name, + Value: "", + }) + } + + appAuth := AppAuthenticationStorage{ + Active: true, + Label: fmt.Sprintf("default_%s", outerapp.Name), + Id: uuid.NewV4().String(), + App: outerapp, + Fields: authFields, + Usage: []AuthenticationUsage{}, + WorkflowCount: 0, + NodeCount: 0, + OrgId: user.ActiveOrg.Id, + Created: timeNow, + Edited: timeNow, + } + + err = setWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) + if err != nil { + log.Printf("Failed setting appauth for with name %s", appAuth.Label) + } + } - //log.Printf("FOUND APP TO VALIDATE: %#v", outerapp) action.Errors = append(action.Errors, "Requires authentication") action.IsValid = false workflow.IsValid = false @@ -2259,7 +2301,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { newActions = actionFixing } - workflow.PreviouslySaved = true + //workflow.PreviouslySaved = true } //PreviouslySaved bool `json:"first_save" datastore:"first_save"` @@ -4616,7 +4658,6 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { resp.Write(data) } -//r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", setAuthenticationConfig).Methods("POST", "OPTIONS") func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -4649,7 +4690,6 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { fileId = location[5] } - log.Printf("FILE: %s", fileId) body, err := ioutil.ReadAll(request.Body) if err != nil { @@ -4679,7 +4719,6 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("body: %s", string(body)) ctx := context.Background() auth, err := getWorkflowAppAuthDatastore(ctx, fileId) if err != nil { @@ -4868,6 +4907,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { //appAuth.LargeImage = "" appAuth.OrgId = user.ActiveOrg.Id + appAuth.Defined = true err = setWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) if err != nil { log.Printf("Failed setting up app auth %s: %s", appAuth.Id, err) diff --git a/docker-compose.yml b/docker-compose.yml index 92a92437..c6632614 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: '3' services: frontend: - #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.51 + build: ./frontend + image: ghcr.io/frikky/shuffle-frontend:0.8.53 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.52 + image: ghcr.io/frikky/shuffle-backend:0.8.53 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd6df28b..e5e02309 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,6 +1,6 @@ { "name": "shuffler", - "version": "0.6.0", + "version": "0.8.53", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -336,6 +336,7 @@ "@babel/highlight": "^7.10.1" } }, + "@babel/core": {}, "@babel/helper-function-name": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", @@ -2566,37 +2567,6 @@ "@babel/helper-plugin-utils": "^7.10.1" } }, - "@babel/plugin-transform-runtime": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.0.tgz", - "integrity": "sha512-pUu9VSf3kI1OqbWINQ7MaugnitRss1z533436waNXp+0N3ur3zfut37sXiQMxkuCF4VUjwZucen/quskCh7NHw==", - "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "resolve": "^1.8.1", - "semver": "^5.5.1" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz", - "integrity": "sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg==", - "requires": { - "@babel/types": "^7.10.1" - } - }, - "@babel/types": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", - "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", - "requires": { - "@babel/helper-validator-identifier": "^7.10.1", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } - } - }, "@babel/plugin-transform-shorthand-properties": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz", @@ -3393,6 +3363,16 @@ "react-is": "^16.7.0" } }, + "jss": { + "dependencies": { + "warning": {} + } + }, + "jss-nested": { + "dependencies": { + "warning": {} + } + }, "popper.js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", @@ -4088,11 +4068,6 @@ } } }, - "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==" - }, "acorn-walk": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", @@ -4706,6 +4681,7 @@ "slash": "^2.0.0" }, "dependencies": { + "@babel/core": {}, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -4747,25 +4723,6 @@ } } }, - "babel-loader": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", - "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", - "requires": { - "find-cache-dir": "^2.1.0", - "loader-utils": "^1.4.0", - "mkdirp": "^0.5.3", - "pify": "^4.0.1", - "schema-utils": "^2.6.5" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - } - } - }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -4971,6 +4928,202 @@ "babel-plugin-transform-react-remove-prop-types": "0.4.24" }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "requires": { + "@babel/types": "^7.12.11", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", + "requires": { + "@babel/types": "^7.12.10" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", + "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-map": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-function-name": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "requires": { + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "requires": { + "@babel/types": "^7.12.10" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "requires": { + "@babel/types": "^7.12.7" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/helper-module-imports": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz", @@ -4979,6 +5132,106 @@ "@babel/types": "^7.10.1" } }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "requires": { + "@babel/types": "^7.12.10" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-replace-supers": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "requires": { + "@babel/types": "^7.12.11" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + } + } + }, + "@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==" + }, "@babel/plugin-proposal-class-properties": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", @@ -5006,6 +5259,31 @@ "@babel/plugin-syntax-numeric-separator": "^7.8.3" } }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.12.1" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + }, + "@babel/plugin-transform-parameters": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", + "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + } + } + }, "@babel/plugin-proposal-optional-chaining": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", @@ -5015,6 +5293,83 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, + "@babel/plugin-syntax-async-generators": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-syntax-json-strings": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-transform-classes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", + "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", + "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-transform-new-target": { + "dependencies": { + "@babel/core": {} + } + }, "@babel/plugin-transform-react-display-name": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz", @@ -5023,6 +5378,37 @@ "@babel/helper-plugin-utils": "^7.8.3" } }, + "@babel/plugin-transform-react-jsx": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-transform-react-jsx-self": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-transform-react-jsx-source": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-transform-regenerator": { + "dependencies": { + "@babel/core": {} + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.0.tgz", + "integrity": "sha512-pUu9VSf3kI1OqbWINQ7MaugnitRss1z533436waNXp+0N3ur3zfut37sXiQMxkuCF4VUjwZucen/quskCh7NHw==", + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "resolve": "^1.8.1", + "semver": "^5.5.1" + } + }, "@babel/preset-env": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.0.tgz", @@ -5088,6 +5474,63 @@ "invariant": "^2.2.2", "levenary": "^1.1.1", "semver": "^5.5.0" + }, + "dependencies": { + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", + "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", + "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", + "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", + "requires": { + "regenerator-transform": "^0.14.2" + } + } } }, "@babel/preset-react": { @@ -5101,6 +5544,95 @@ "@babel/plugin-transform-react-jsx-development": "^7.9.0", "@babel/plugin-transform-react-jsx-self": "^7.9.0", "@babel/plugin-transform-react-jsx-source": "^7.9.0" + }, + "dependencies": { + "@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "requires": { + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz", + "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz", + "integrity": "sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.10", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.12.1", + "@babel/types": "^7.12.12" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz", + "integrity": "sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz", + "integrity": "sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + } + } + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/runtime": { @@ -5111,6 +5643,66 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/traverse": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "requires": { + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/types": { "version": "7.10.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", @@ -5121,10 +5713,76 @@ "to-fast-properties": "^2.0.0" } }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "babel-loader": {}, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "regenerator-runtime": { "version": "0.13.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + }, + "regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -6401,6 +7059,17 @@ "tar-pack": "3.4.1", "tmp": "0.0.33", "validate-npm-package-name": "3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } } }, "create-react-class": { @@ -6422,15 +7091,6 @@ "warning": "^4.0.3" } }, - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -8204,6 +8864,13 @@ "acorn": "^7.1.1", "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==" + } } }, "esprima": { @@ -11669,24 +12336,6 @@ "resolved": "https://registry.npmjs.org/jss-global/-/jss-global-3.0.0.tgz", "integrity": "sha512-wxYn7vL+TImyQYGAfdplg7yaxnPQ9RaXY/cIA8hawaVnmmWxDHzBK32u1y+RAvWboa3lW83ya3nVZ/C+jyjZ5Q==" }, - "jss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jss-nested/-/jss-nested-6.0.1.tgz", - "integrity": "sha512-rn964TralHOZxoyEgeq3hXY8hyuCElnvQoVrQwKHVmu55VRDd6IqExAx9be5HgK0yN/+hQdgAXQl/GUrBbbSTA==", - "requires": { - "warning": "^3.0.0" - }, - "dependencies": { - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } - } - } - }, "jss-plugin-camel-case": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.1.1.tgz", @@ -12182,19 +12831,6 @@ "resolved": "https://registry.npmjs.org/material-ui-nested-menu-item/-/material-ui-nested-menu-item-1.0.2.tgz", "integrity": "sha512-LZb8xI0FrAI/A3P2vT3CB9bmSoOFWOK0dikTc1t9VvEpp1a8hZkbVUz7VhETnoLUYu3NXCkgulmXcl3zitqI9A==" }, - "material-ui-pickers": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/material-ui-pickers/-/material-ui-pickers-2.2.4.tgz", - "integrity": "sha512-QCQh08Ylmnt+o4laW+rPs92QRAcESv3sPXl50YadLm++rAZAXAOh3K8lreGdynCMYFgZfdyu81Oz9xzTlAZNfw==", - "requires": { - "@types/react-text-mask": "^5.4.3", - "clsx": "^1.0.2", - "react-event-listener": "^0.6.6", - "react-text-mask": "^5.4.3", - "react-transition-group": "^2.5.3", - "tslib": "^1.9.3" - } - }, "md5-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-4.0.0.tgz", @@ -12277,6 +12913,24 @@ "warning": "^4.0.1" }, "dependencies": { + "jss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jss-nested/-/jss-nested-6.0.1.tgz", + "integrity": "sha512-rn964TralHOZxoyEgeq3hXY8hyuCElnvQoVrQwKHVmu55VRDd6IqExAx9be5HgK0yN/+hQdgAXQl/GUrBbbSTA==", + "requires": { + "warning": "^3.0.0" + }, + "dependencies": { + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "react-transition-group": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", @@ -12339,6 +12993,37 @@ } } }, + "jss-nested": { + "dependencies": { + "warning": {} + } + }, + "material-ui-pickers": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/material-ui-pickers/-/material-ui-pickers-2.2.4.tgz", + "integrity": "sha512-QCQh08Ylmnt+o4laW+rPs92QRAcESv3sPXl50YadLm++rAZAXAOh3K8lreGdynCMYFgZfdyu81Oz9xzTlAZNfw==", + "requires": { + "@types/react-text-mask": "^5.4.3", + "clsx": "^1.0.2", + "react-event-listener": "^0.6.6", + "react-text-mask": "^5.4.3", + "react-transition-group": "^2.5.3", + "tslib": "^1.9.3" + }, + "dependencies": { + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + } + } + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -12393,7 +13078,8 @@ "loose-envify": "^1.4.0", "prop-types": "^15.6.2" } - } + }, + "sass-loader": {} } }, "mdn-data": { @@ -13806,6 +14492,19 @@ "requires": { "postcss": "^7.0.2", "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + } } }, "postcss-browser-comments": { @@ -13826,6 +14525,17 @@ "postcss-value-parser": "^4.0.2" }, "dependencies": { + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, "postcss-value-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", @@ -14889,13 +15599,12 @@ } }, "react": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", - "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", + "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" } }, "react-alert": { @@ -15306,14 +16015,24 @@ } }, "react-dom": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz", - "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", + "integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.16.2" + "scheduler": "^0.20.1" + }, + "dependencies": { + "scheduler": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz", + "integrity": "sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } } }, "react-draggable": { @@ -15584,6 +16303,49 @@ "workbox-webpack-plugin": "4.3.1" }, "dependencies": { + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, "@babel/generator": { "version": "7.11.4", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", @@ -15887,6 +16649,46 @@ "resolve": "^1.12.0" } }, + "babel-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "requires": { + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + } + } + }, "cacache": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", @@ -15982,6 +16784,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -16115,6 +16922,19 @@ "classnames": "^2.2.6", "prop-types": "^15.7.2", "react-transition-group": "^2.6.1" + }, + "dependencies": { + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + } } }, "react-transition-group": { @@ -16852,15 +17672,6 @@ "xmlchars": "^2.1.1" } }, - "scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "schema-utils": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b627526f..d2ff0059 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,13 +1,13 @@ { "name": "shuffler", "homepage": "https://shuffler.io", - "version": "0.8.3", + "version": "0.8.53", "private": true, "dependencies": { "@material-ui/core": "^4.5.2", "@material-ui/icons": "^4.5.1", "@material-ui/styles": "^4.5.2", - "@use-it/interval": "^0.1.3", + "@use-it/interval": "^1.0.0", "babel-eslint": "^10.1.0", "class-transformer": "^0.3.1", "create-react-app": "^2.0.3", @@ -32,7 +32,7 @@ "md5-file": "^4.0.0", "mdbreact": "^4.21.1", "moment": "~2.20.1", - "react": "^16.10.2", + "react": "^17.0.1", "react-alert": "^5.5.0", "react-alert-template-basic": "^1.0.0", "react-beforeunload": "^2.2.1", @@ -40,7 +40,7 @@ "react-cookie": "^4.0.1", "react-cytoscapejs": "^1.2.0", "react-device-detect": "^1.9.10", - "react-dom": "^16.10.2", + "react-dom": "^17.0.1", "react-draggable": "^3.3.2", "react-dropzone": "^10.1.10", "react-ga": "^2.7.0", From 6f945d1a6887884def9e07aa20fb876812890735 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 18 Jan 2021 10:04:02 +0100 Subject: [PATCH 064/185] Fixed API authentication for files --- .env | 2 +- backend/go-app/files.go | 3 +- backend/go-app/main.go | 6 +-- backend/go-app/walkoff.go | 61 +++++++++++++++++++------- docker-compose.yml | 6 +-- frontend/Dockerfile | 7 +-- frontend/src/views/Admin.jsx | 37 +++++++++++----- frontend/src/views/AngularWorkflow.jsx | 11 ++++- 8 files changed, 95 insertions(+), 38 deletions(-) diff --git a/.env b/.env index 45389f1f..5fdb2e83 100644 --- a/.env +++ b/.env @@ -29,7 +29,7 @@ BACKEND_PORT=5001 FRONTEND_PORT=3001 FRONTEND_PORT_HTTPS=3443 OUTER_HOSTNAME=shuffle-backend -DB_LOCATION=./shuffle-database +DB_LOCATION=./shuffle-database-new # Proxy configurations. SHUFFLE_PASS_WORKER_PROXY must be FALSE to not pass the proxy information to sub-apps. # PS: It will skip proxy for diff --git a/backend/go-app/files.go b/backend/go-app/files.go index 7312af1f..0779a08c 100644 --- a/backend/go-app/files.go +++ b/backend/go-app/files.go @@ -701,7 +701,8 @@ func handleCreateFile(resp http.ResponseWriter, request *http.Request) { // Loads of validation below if len(curfile.Filename) == 0 || len(curfile.OrgId) == 0 || len(curfile.WorkflowId) == 0 { - log.Printf("[ERROR] Missing field during upload.") + log.Printf("[ERROR] Missing field during fileupload. Required: filename, org_id, workflow_id") + log.Printf("INPUT: %s", string(body)) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Missing field. Required: filename, org_id, workflow_id"}`))) return diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 6e304dd2..f19679e0 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -614,13 +614,13 @@ func handleApiAuthentication(resp http.ResponseWriter, request *http.Request) (U apikey := request.Header.Get("Authorization") if len(apikey) > 0 { if !strings.HasPrefix(apikey, "Bearer ") { - log.Printf("Apikey doesn't start with bearer") + log.Printf("[WARNING] Apikey doesn't start with bearer") return User{}, errors.New("No bearer token for authorization header") } apikeyCheck := strings.Split(apikey, " ") if len(apikeyCheck) != 2 { - log.Printf("Invalid format for apikey.") + log.Printf("[WARNING] Invalid format for apikey.") return User{}, errors.New("Invalid format for apikey") } @@ -643,7 +643,7 @@ func handleApiAuthentication(resp http.ResponseWriter, request *http.Request) (U if len(Userdata.Username) > 0 { return Userdata, nil } else { - return Userdata, errors.New(fmt.Sprintf("User is invalid - no username found")) + return Userdata, errors.New(fmt.Sprintf("[WARNING] User is invalid - no username found")) } } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 0c5e2fc4..b8804c39 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -909,8 +909,8 @@ func validateNewWorkerExecution(body []byte) error { return err } - log.Printf("LEN: %s", string(body)) - log.Printf("LEN: %d", len(string(body))) + //log.Printf("LEN: %s", string(body)) + //log.Printf("LEN: %d", len(string(body))) baseExecution, err := getWorkflowExecution(ctx, execution.ExecutionId) if err != nil { log.Printf("[ERROR] Failed getting execution (workflowqueue) %s: %s", execution.ExecutionId, err) @@ -1710,7 +1710,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { user.ActiveOrg.Users = []User{} workflow.ExecutingOrg = user.ActiveOrg workflow.OrgId = user.ActiveOrg.Id - log.Printf("TRIGGERS: %d", len(workflow.Triggers)) + //log.Printf("TRIGGERS: %d", len(workflow.Triggers)) ctx := context.Background() //err = increaseStatisticsField(ctx, "total_workflows", workflow.ID, 1, workflow.OrgId) @@ -1744,7 +1744,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { // Initialized without functions = adding a hello world node. if len(newActions) == 0 { - log.Printf("APPENDING NEW APP FOR NEW WORKFLOW") + //log.Printf("APPENDING NEW APP FOR NEW WORKFLOW") // Adds the Testing app if it's a new workflow workflowapps, err := getAllWorkflowApps(ctx) @@ -2188,8 +2188,10 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflowapps, apperr := getAllWorkflowApps(ctx) allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - if err == nil && len(allAuths) > 0 && len(workflowapps) > 0 && apperr == nil { + if err == nil && len(workflowapps) > 0 && apperr == nil { + log.Printf("Setting actions") actionFixing := []Action{} + appsAdded := []string{} for _, action := range newActions { setAuthentication := false if len(action.AuthenticationId) > 0 { @@ -2253,6 +2255,12 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } + for _, added := range appsAdded { + if outerapp.ID == added { + found = true + } + } + // FIXME: Add app auth if !found { timeNow := int64(time.Now().Unix()) @@ -2281,6 +2289,8 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { err = setWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) if err != nil { log.Printf("Failed setting appauth for with name %s", appAuth.Label) + } else { + appsAdded = append(appsAdded, outerapp.ID) } } @@ -2299,14 +2309,16 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } newActions = actionFixing + } else { + log.Printf("Err: %s - %s", err, apperr) + //workflowapps, apperr := getAllWorkflowApps(ctx) + //allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) } - //workflow.PreviouslySaved = true + workflow.PreviouslySaved = true } - //PreviouslySaved bool `json:"first_save" datastore:"first_save"` workflow.Actions = newActions - newTriggers := []Trigger{} for _, trigger := range workflow.Triggers { log.Printf("Trigger %s: %s", trigger.TriggerType, trigger.Status) @@ -3421,15 +3433,19 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf return WorkflowExecution{}, "Failed building missing Docker images", err } - b, err := json.Marshal(workflowExecution) - if err == nil { - log.Printf("%s", string(b)) - log.Printf("LEN: %d", len(string(b))) - //workflowExecution.ExecutionOrg.SyncFeatures = Org{} - } + //b, err := json.Marshal(workflowExecution) + //if err == nil { + // log.Printf("%s", string(b)) + // log.Printf("LEN: %d", len(string(b))) + // //workflowExecution.ExecutionOrg.SyncFeatures = Org{} + //} - workflowExecution.Workflow.ExecutingOrg = Org{} - workflowExecution.Workflow.Org = []Org{} + workflowExecution.Workflow.ExecutingOrg = Org{ + Id: workflowExecution.Workflow.ExecutingOrg.Id, + } + workflowExecution.Workflow.Org = []Org{ + workflowExecution.Workflow.ExecutingOrg, + } //Org []Org `json:"org,omitempty" datastore:"org"` err = setWorkflowExecution(ctx, workflowExecution, true) if err != nil { @@ -4735,6 +4751,7 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { } if config.Action == "assign_everywhere" { + log.Printf("Should set authentication config") q := datastore.NewQuery("workflow").Filter("org_id =", user.ActiveOrg.Id) q = q.Order("-edited").Limit(35) @@ -4750,14 +4767,21 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { // FIXME: Add function to remove auth from other auth's actionCnt := 0 workflowCnt := 0 + authenticationUsage := []AuthenticationUsage{} for _, workflow := range workflows { newActions := []Action{} edited := false + usage := AuthenticationUsage{ + WorkflowId: workflow.ID, + Nodes: []string{}, + } + for _, action := range workflow.Actions { if action.AppName == auth.App.Name { //log.Printf("FOUND ACTION TO UPDATE: %#v", action) edited = true actionCnt += 1 + usage.Nodes = append(usage.Nodes, action.ID) } newActions = append(newActions, action) @@ -4765,6 +4789,8 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { workflow.Actions = newActions if edited { + //auth.Usage = usage + authenticationUsage = append(authenticationUsage, usage) err = setWorkflow(ctx, workflow, workflow.ID) if err != nil { log.Printf("Failed setting (authupdate) workflow: %s", err) @@ -4775,9 +4801,12 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { } } + //Usage []AuthenticationUsage `json:"usage" datastore:"usage"` + log.Printf("Found %d workflows, %d actions", workflowCnt, actionCnt) if actionCnt > 0 && workflowCnt > 0 { auth.WorkflowCount = int64(workflowCnt) auth.NodeCount = int64(actionCnt) + auth.Usage = authenticationUsage err = setWorkflowAppAuthDatastore(ctx, *auth, auth.Id) if err != nil { diff --git a/docker-compose.yml b/docker-compose.yml index c6632614..1ce245a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - build: ./frontend + #build: ./frontend image: ghcr.io/frikky/shuffle-frontend:0.8.53 container_name: shuffle-frontend hostname: shuffle-frontend @@ -16,8 +16,8 @@ services: depends_on: - backend backend: - #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.53 + build: ./backend + image: ghcr.io/frikky/shuffle-backend:0.8.54 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cc791b82..f48b048d 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -8,7 +8,8 @@ ENV PATH /usr/src/app/node_modules/.bin:$PATH COPY package.json /usr/src/app/package.json -RUN npm install --verbose +#RUN npm install --verbose +RUN yarn install # copy only required files to not trigger rebuilding every time COPY ./certs /usr/src/app/certs/ @@ -19,9 +20,9 @@ COPY ./*.json /usr/src/app/ # There were issues with the webpack installer from package.json RUN rm -rf /usr/src/app/node_modules/webpack -RUN npm install webpack@4.42.0 +#RUN yarn add webpack@4.42.0 -RUN npm run-script build +RUN yarn build # Production environment FROM nginx:latest diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index ae7ec058..bc94a537 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -301,6 +301,7 @@ const Admin = (props) => { } else { //alert.success("Successfully password!") setSelectedUserModalOpen(false) + getAppAuthentication() } }), ) @@ -762,6 +763,7 @@ const Admin = (props) => { .then((responseJson) => { if (responseJson.success) { //console.log(responseJson.data) + console.log(responseJson) setAuthentication(responseJson.data) } else { alert.error("Failed getting authentications") @@ -2033,7 +2035,7 @@ const Admin = (props) => {

App Authentication

Control the authentication options for individual apps. Actions can be destructive! - . Learn more +  Learn more
@@ -2087,7 +2089,7 @@ const Admin = (props) => { style={{minWidth: 150, maxWidth: 150}} /> { > - { - editAuthenticationConfig(data.id) - }} - > - - + {data.defined ? + + { + editAuthenticationConfig(data.id) + }} + > + + + + : + + { + }} + > + + + + } { deleteAuthentication(data) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index a1dead44..d860d561 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -948,7 +948,16 @@ const AngularWorkflow = (props) => { }) .then((responseJson) => { if (responseJson.success) { - setAppAuthentication(responseJson.data) + var newauth = [] + for (var key in responseJson.data) { + if (responseJson.data[key].defined === false) { + continue + } + + newauth.push(responseJson.data[key]) + } + + setAppAuthentication(newauth) } else { alert.error("Failed getting authentications") } From 79b6c6444c1c8aae6d4d96c6eb04f51920dd08e9 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 18 Jan 2021 10:08:53 +0100 Subject: [PATCH 065/185] Pushed new backend --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1ce245a3..9dec03cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: depends_on: - backend backend: - build: ./backend + #build: ./backend image: ghcr.io/frikky/shuffle-backend:0.8.54 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} From 5338bf24859a20c323da388fec8ac7323b8bd83a Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 18 Jan 2021 16:09:01 +0100 Subject: [PATCH 066/185] #204: Added file upload baseline to app creator --- backend/app_sdk/app_base.py | 8 +++--- backend/go-app/codegen.go | 25 ++++++++++++++---- frontend/src/views/AppCreator.jsx | 43 +++++++++++++++++++++++++++++++ frontend/src/views/Apps.jsx | 16 +++++++----- 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 14936845..77ccdb4c 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -397,7 +397,7 @@ class AppBase: } ret1 = requests.get("%s%s" % (self.url, get_path), headers=headers) - print("RET1: %s" % ret1.text) + print("RET1 (file get): %s" % ret1.text) if ret1.status_code != 200: returns.append({ "filename": "", @@ -408,7 +408,7 @@ class AppBase: content_path = "/api/v1/files/%s/content?execution_id=%s" % (item, full_execution["execution_id"]) ret2 = requests.get("%s%s" % (self.url, content_path), headers=headers) - print("Ret2: %s" % ret2.text) + print("RET2 (file get): %s" % ret2.text) if ret2.status_code == 200: tmpdata = ret1.json() returndata = { @@ -418,6 +418,8 @@ class AppBase: } returns.append(returndata) + print("RET3 (file get done)") + if len(returns) == 0: return { "success": False, @@ -1687,7 +1689,7 @@ class AppBase: print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") print("[INFO] Running with params (0): %s" % params) newres = await func(**params) - print("[INFO] Returned from execution.") + print("[INFO] Returned from execution:", newres) if isinstance(newres, tuple): print("[INFO] Handling return as tuple") # Handles files. diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index ba029b15..2c7c64d7 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -365,6 +365,16 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet preparedHeaders += "}" } + fileBalance := "" + fileAdder := `` + fileGrabber := `` + if method == "post" && strings.Contains(functionname, "filescan") { + fileGrabber = `filedata = self.get_file("63264006-5958-451a-bff1-1975495fb4d8")` + //fileGrabber += "\n print(filedata)" + fileAdder = `files = {"file": (filedata["filename"], filedata["data"])}` + fileBalance = ", files=files" + } + // Extra param for url if it's changeable // Extra param for authentication scheme(s) // The last weird one is the body.. Tabs & spaces sucks. @@ -375,7 +385,9 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet %s %s %s - return requests.%s(url, headers=headers%s%s%s).text + %s + %s + return requests.%s(url, headers=headers%s%s%s%s).text `, functionname, authenticationParameter, @@ -391,17 +403,20 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet authenticationSetup, queryData, bodyFormatter, + fileGrabber, + fileAdder, method, authenticationAddin, bodyAddin, verifyAddin, + fileBalance, ) //log.Printf("FUNCTION: %s", data) - //if strings.Contains(functionname, "search") { - // log.Println(data) - // log.Printf("Queries: %s", queryString) - //} + if strings.Contains(functionname, "filescan") { + log.Println(data) + log.Printf("Queries: %s", queryString) + } //log.Printf(data) return functionname, data diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 3171610a..a4fabc8e 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -226,6 +226,7 @@ const AppCreator = (props) => { const [errorCode, setErrorCode] = useState("") const [appBuilding, setAppBuilding] = useState(false) const [extraBodyFields, setExtraBodyFields] = useState([]) + const [fileUploadEnabled, setFileUploadEnabled] = useState(false) //const [actions, setActions] = useState([{ // "name": "Get workflows", @@ -254,6 +255,7 @@ const AppCreator = (props) => { const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0]) const [currentAction, setCurrentAction] = useState({ "name": "", + "file_field": "", "description": "", "url": "", "headers": "", @@ -494,6 +496,7 @@ const AppCreator = (props) => { "name": tmpname, "description": methodvalue.description, "url": path, + "file_field": "", "method": method.toUpperCase(), "headers": "", "queries": [], @@ -959,6 +962,11 @@ const AppCreator = (props) => { data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) } + // https://swagger.io/docs/specification/describing-request-body/file-upload/ + if (item.file_field !== undefined && item.file_field !== null && item.file_field.length > 0) { + console.log("HANDLE FILEFIELD SAVE: ", item.file_field) + } + if (item.headers.length > 0) { const required = false @@ -1171,6 +1179,7 @@ const AppCreator = (props) => { "name": "", "description": "", "url": "", + "file_field": "", "headers": "", "paths": [], "queries": [], @@ -1609,6 +1618,7 @@ const AppCreator = (props) => { "name": "", "description": "", "url": "", + "file_field": "", "headers": "", "paths": [], "queries": [], @@ -1619,6 +1629,7 @@ const AppCreator = (props) => { setCurrentActionMethod(apikeySelection[0]) setUrlPathQueries([]) setActionsModalOpen(false) + setFileUploadEnabled(false) }} > @@ -1806,6 +1817,36 @@ const AppCreator = (props) => { + {currentActionMethod === "POST" ? + + : null} + {fileUploadEnabled ? + setActionField("file_field", e.target.value)} + helperText={The File field to interact with} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> + : null}
Headers: static for the action { setActionsModalOpen(false) setUrlPathQueries([]) setUrlPath("") + setFileUploadEnabled(false) }}> Submit @@ -1916,6 +1958,7 @@ const AppCreator = (props) => { "name": "", "description": "", "url": "", + "file_field": "", "headers": "", "queries": [], "paths": [], diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 97689cac..bcee8fe5 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -807,12 +807,16 @@ const Apps = (props) => { const reader = new FileReader(); - reader.addEventListener('load', (e) => { - const content = e.target.result; - setOpenApiData(content); - setIsDropzone(isDropzone); - setOpenApiModal(true) - }) + try { + reader.addEventListener('load', (e) => { + const content = e.target.result; + setOpenApiData(content); + setIsDropzone(isDropzone); + setOpenApiModal(true) + }) + } catch (e) { + console.log("Error in dropzone: ", e) + } reader.readAsText(files[0]); }; From 792a0b86caacc2e5666863cd4141b67702e89cde Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 18 Jan 2021 16:18:19 +0100 Subject: [PATCH 067/185] Rolled back env --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 5fdb2e83..45389f1f 100644 --- a/.env +++ b/.env @@ -29,7 +29,7 @@ BACKEND_PORT=5001 FRONTEND_PORT=3001 FRONTEND_PORT_HTTPS=3443 OUTER_HOSTNAME=shuffle-backend -DB_LOCATION=./shuffle-database-new +DB_LOCATION=./shuffle-database # Proxy configurations. SHUFFLE_PASS_WORKER_PROXY must be FALSE to not pass the proxy information to sub-apps. # PS: It will skip proxy for From 42bfa92c81ed432788d7eff9085a332e91fc263e Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 20 Jan 2021 16:03:01 +0100 Subject: [PATCH 068/185] Added tracker of selected executions --- backend/app_sdk/app_base.py | 2 +- backend/go-app/codegen.go | 4 ++-- backend/go-app/docker.go | 2 +- backend/go-app/walkoff.go | 14 ++++++------ docker-compose.yml | 2 +- frontend/src/views/AngularWorkflow.jsx | 30 +++++++++++++++++++++++--- frontend/src/views/AppCreator.jsx | 7 +++--- functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/orborus.go | 18 ++++++++++------ functions/onprem/worker/worker.go | 4 ++++ 10 files changed, 60 insertions(+), 25 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 77ccdb4c..23c695eb 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -601,7 +601,7 @@ class AppBase: self.full_execution = fullexecution - self.logger.info("AFTER FULLEXEC stream result") + self.logger.info("AFTER FULLEXEC stream result (init)") # Gets the value at the parenthesis level you want def parse_nested_param(string, level): diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 2c7c64d7..ee182ef6 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -412,8 +412,8 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet fileBalance, ) - //log.Printf("FUNCTION: %s", data) - if strings.Contains(functionname, "filescan") { + if strings.Contains(functionname, "get_list_rulesssss") { + //log.Printf("FUNCTION: %s", data) log.Println(data) log.Printf("Queries: %s", queryString) } diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index f05e4d38..0a0b4bae 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -190,7 +190,7 @@ func fixTags(tags []string) []string { */ // Custom Docker image builder wrapper in memory -func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder string) error { +func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder string, downloadIfFail bool) error { ctx := context.Background() client, err := client.NewEnvClient() if err != nil { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index b8804c39..4a468d29 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -909,8 +909,6 @@ func validateNewWorkerExecution(body []byte) error { return err } - //log.Printf("LEN: %s", string(body)) - //log.Printf("LEN: %d", len(string(body))) baseExecution, err := getWorkflowExecution(ctx, execution.ExecutionId) if err != nil { log.Printf("[ERROR] Failed getting execution (workflowqueue) %s: %s", execution.ExecutionId, err) @@ -6421,7 +6419,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin if len(extra) == 0 { log.Printf("[INFO] Starting build of %d containers (FIRST)", len(buildLaterFirst)) for _, item := range buildLaterFirst { - err = buildImageMemory(fs, item.Tags, item.Extra) + err = buildImageMemory(fs, item.Tags, item.Extra, true) if err != nil { log.Printf("Failed image build memory: %s", err) } else { @@ -6435,7 +6433,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin log.Printf("[INFO] Starting build of %d skipped docker images", len(buildLaterList)) for _, item := range buildLaterList { - err = buildImageMemory(fs, item.Tags, item.Extra) + err = buildImageMemory(fs, item.Tags, item.Extra, true) if err != nil { log.Printf("[INFO] Failed image build memory: %s", err) } else { @@ -6647,15 +6645,19 @@ func getAllSchedules(ctx context.Context, orgId string) ([]ScheduleOld, error) { return schedules, nil } +//FIXME: Add cursor func getAllWorkflowApps(ctx context.Context) ([]WorkflowApp, error) { var allworkflowapps []WorkflowApp - q := datastore.NewQuery("workflowapp").Order("-edited").Limit(50) + q := datastore.NewQuery("workflowapp").Order("-edited").Limit(40) + //Activated bool `json:"activated" yaml:"activated" required:false datastore:"activated"` //Activated bool `json:"activated" yaml:"activated" required:false datastore:"activated"` _, err := dbclient.GetAll(ctx, q, &allworkflowapps) if err != nil { if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { - q := datastore.NewQuery("workflowapp").Limit(40).Order("-edited") + //datastore.NewQuery("workflowapp").Limit(30).Order("-edited") + q = datastore.NewQuery("workflowapp").Order("-edited").Limit(27) + //q := q.Limit(25) _, err := dbclient.GetAll(ctx, q, &allworkflowapps) if err != nil { return []WorkflowApp{}, err diff --git a/docker-compose.yml b/docker-compose.yml index 9dec03cb..1ce245a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: depends_on: - backend backend: - #build: ./backend + build: ./backend image: ghcr.io/frikky/shuffle-backend:0.8.54 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index d860d561..6605e386 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -153,6 +153,7 @@ const AngularWorkflow = (props) => { const [requiresAuthentication, setRequiresAuthentication] = React.useState(false) const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false) const [showSkippedActions, setShowSkippedActions] = React.useState(false) + const [lastExecution, setLastExecution] = React.useState("") const [selectedResult, setSelectedResult] = React.useState({}) const [codeModalOpen, setCodeModalOpen] = React.useState(false); @@ -6380,13 +6381,14 @@ const AngularWorkflow = (props) => { - + {workflowExecutions.length > 0 ?
{workflowExecutions.map((data, index) => { @@ -6405,6 +6407,7 @@ const AngularWorkflow = (props) => { } return ( + {}} onMouseOut={() => {}} onClick={() => { if (data.result === undefined || data.result === null || data.result.length === 0) { @@ -6420,7 +6423,7 @@ const AngularWorkflow = (props) => { setExecutionData(data) }}>
-
+
{getExecutionSourceImage(data)}
@@ -6436,9 +6439,14 @@ const AngularWorkflow = (props) => { : null}
- + {lastExecution === data.execution_id ? + + : + + } + ) return })} @@ -6457,6 +6465,7 @@ const AngularWorkflow = (props) => { stop() getWorkflowExecution(props.match.params.key) setExecutionModalView(0) + setLastExecution(executionData.execution_id) }}> {}}> @@ -6617,6 +6626,21 @@ const AngularWorkflow = (props) => { validate.result = JSON.parse(validate.result) } + //if (codeModalOpen && selectedResult.result.includes("file_id")) { + // console.log("SHOW RESULT WITH FILES: ", selectedResult.result) + // //const regex = "\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b" + // //const regex = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i + // const regex = /^[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}$/i + // //const found = selectedResult.result.match(regex) + // const found = "hello how are you cf80fa70-65cf-4963-b474-b459a6dead81 what".match(regex) + // const regex = /\${(\w{8}-\w{4}-\w{3}-\w{3}-\w)}/g + // const found = placeholder.match(regex) + + // console.log("FOUND: ", found) + + // //cf80fa70-65cf-4963-b474-b459a6dead81 + //} + const codePopoutModal = !codeModalOpen ? null : { diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index a4fabc8e..82c28446 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -479,11 +479,12 @@ const AppCreator = (props) => { for (let [path, pathvalue] of Object.entries(data.paths)) { for (let [method, methodvalue] of Object.entries(pathvalue)) { if (methodvalue === null) { - alert.info("Skipped method "+method) + alert.info("Skipped method (null)"+method) continue } if (!allowedfunctions.includes(method.toUpperCase())) { + alert.info("Skipped method (not allowed) "+method) continue } @@ -556,7 +557,7 @@ const AppCreator = (props) => { } // HAHAHA wtf is this. - if (methodvalue.responses !== undefined) { + if (methodvalue.responses !== undefined && methodvalue.responses !== null) { if (methodvalue.responses.default !== undefined) { if (methodvalue.responses.default.content !== undefined) { if (methodvalue.responses.default.content["text/plain"] !== undefined) { @@ -1818,7 +1819,7 @@ const AppCreator = (props) => { addPathQuery() }}>New query {currentActionMethod === "POST" ? - {currentActionMethod === "POST" ? - From 09002f4aa316e66a702745e2cc6ef3e16988d355 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 22 Jan 2021 13:27:28 +0100 Subject: [PATCH 077/185] Moved executions to workflow view --- backend/go-app/walkoff.go | 1 - frontend/src/views/AngularWorkflow.jsx | 6 ++++++ frontend/src/views/Workflows.jsx | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 66555157..fa8389ea 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -4940,7 +4940,6 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { if foundIndex >= 0 { log.Printf("[INFO] Found app %s by looping auth", appAuth.App.ID) - app = &workflowapps[foundIndex] } else { log.Printf("[ERROR] Failed finding app %s which has auth after looping", appAuth.App.ID) resp.WriteHeader(409) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 36c07f4e..9e494d91 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1536,6 +1536,12 @@ const AngularWorkflow = (props) => { getWorkflowExecution(props.match.params.key) getAvailableWorkflows(-1) getSettings() + + const cursearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search + const tmpView = new URLSearchParams(cursearch).get("view") + if (tmpView !== undefined && tmpView !== null && tmpView === "executions") { + setExecutionModalOpen(true) + } return } diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index ca0351ec..960a7fbc 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -907,7 +907,7 @@ const Workflows = (props) => {
:

- There are no executions for this workflow yet + Executions have been moved to the Workflow itself. Click here to see them

) } @@ -1247,7 +1247,8 @@ const Workflows = (props) => {
-
+ {/* +

Execution Timeline

@@ -1265,6 +1266,7 @@ const Workflows = (props) => {
+ */}
) } From 73b7469107ed43a2d6ea9703ac0a5fe2af3c233a Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 22 Jan 2021 13:55:04 +0100 Subject: [PATCH 078/185] #223: Added import and export protections for subflow --- backend/go-app/walkoff.go | 39 ++++++++++++++++++++++++++------ frontend/src/views/Workflows.jsx | 25 ++++++++++++++------ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index fa8389ea..3b4db6e5 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2332,15 +2332,40 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { trigger.Status = "stopped" } } else if trigger.TriggerType == "SUBFLOW" { - for _, param := range trigger.Parameters { + for index, param := range trigger.Parameters { if len(param.Value) == 0 && param.Name != "argument" { - workflow.IsValid = false - workflow.Errors = []string{"Trigger is missing a parameter: %s", param.Name} + log.Printf("Param: %#v", param) + if param.Name == "user_apikey" { + apikey := "" + if len(user.ApiKey) > 0 { + apikey = user.ApiKey + } else { + user, err = generateApikey(ctx, user) + if err != nil { + workflow.IsValid = false + workflow.Errors = []string{"Trigger is missing a parameter: %s", param.Name} - log.Printf("No type specified for user input node") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Trigger %s is missing the parameter %s"}`, trigger.Label, param.Name))) - return + log.Printf("No type specified for user input node") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Trigger %s is missing the parameter %s"}`, trigger.Label, param.Name))) + return + } + + apikey = user.ApiKey + } + + log.Printf("[INFO] Set apikey in subflow trigger for user during save") + trigger.Parameters[index].Value = apikey + } else { + + workflow.IsValid = false + workflow.Errors = []string{"Trigger is missing a parameter: %s", param.Name} + + log.Printf("No type specified for user input node") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Trigger %s is missing the parameter %s"}`, trigger.Label, param.Name))) + return + } } } } else if trigger.TriggerType == "WEBHOOK" && trigger.Status != "uninitialized" { diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 960a7fbc..83616e2b 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -234,8 +234,8 @@ const Workflows = (props) => { color: "#ffffff", width: "100%", display: "flex", - minWidth: 1366, - maxWidth: 1766, + minWidth: 1024, + maxWidth: 1024, margin: "auto", maxHeight: "90vh", } @@ -393,8 +393,15 @@ const Workflows = (props) => { data["owner"] = "" for (var key in data.triggers) { - if (data.triggers[key].status == "running") { - data.triggers[key].status = "stopped" + const trigger = data.triggers[key] + if (trigger.app_name === "Shuffle Workflow") { + if (trigger.parameters.length > 2) { + trigger.parameters[2].value = "" + } + } + + if (trigger.status == "running") { + trigger.status = "stopped" } } @@ -402,6 +409,8 @@ const Workflows = (props) => { data.actions[key].authentication_id = "" } + //return + data["org"] = [] data["org_id"] = "" data.execution_org = {"id": ""} @@ -907,7 +916,7 @@ const Workflows = (props) => {
:

- Executions have been moved to the Workflow itself. Click here to see them + Executions have been moved to the Workflow itself.
Click here to see them

) } @@ -1230,10 +1239,11 @@ const Workflows = (props) => {
-
+

Executions: {selectedWorkflow.name}

-
+ {/* +
+ */}
From c413dcece5ea6bd5f2a6cc9ae592795d800e24d0 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 22 Jan 2021 16:31:08 +0100 Subject: [PATCH 079/185] 0.8.56 build update --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index cef90c06..addef547 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.55 + image: ghcr.io/frikky/shuffle-frontend:0.8.56 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.55 + image: ghcr.io/frikky/shuffle-backend:0.8.56 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: From 9b1f7fdce6b40d8e0abd7c3d9d79148ee79ce4d8 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 22 Jan 2021 16:46:31 +0100 Subject: [PATCH 080/185] Fixed env --- .env | 2 +- frontend/src/views/AngularWorkflow.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 5fdb2e83..45389f1f 100644 --- a/.env +++ b/.env @@ -29,7 +29,7 @@ BACKEND_PORT=5001 FRONTEND_PORT=3001 FRONTEND_PORT_HTTPS=3443 OUTER_HOSTNAME=shuffle-backend -DB_LOCATION=./shuffle-database-new +DB_LOCATION=./shuffle-database # Proxy configurations. SHUFFLE_PASS_WORKER_PROXY must be FALSE to not pass the proxy information to sub-apps. # PS: It will skip proxy for diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 9e494d91..f4ef05c4 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1396,7 +1396,7 @@ const AngularWorkflow = (props) => { console.log("ESCAPE") break; case 46: - removeNode() + //removeNode() console.log("DELETE") break; case 38: From 5dd78211d4c45437e7582206547747e2a3811673 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 23 Jan 2021 17:04:52 +0100 Subject: [PATCH 081/185] Added appcreator autocomplete feature for GET {PATH} --- backend/go-app/walkoff.go | 4 +- frontend/src/views/AppCreator.jsx | 24 ++++- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 151 ++++++++++++++++++++---------- 4 files changed, 121 insertions(+), 60 deletions(-) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 3b4db6e5..d12d969d 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -3306,7 +3306,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if len(action.Label) == 0 { action.Label = action.ID } - log.Printf("LABEL: %s", action.Label) + //log.Printf("LABEL: %s", action.Label) newActions = append(newActions, action) // If the node is NOT found, it's supposed to be set to SKIPPED, @@ -3360,7 +3360,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } for _, trigger := range workflowExecution.Workflow.Triggers { - log.Printf("[INFO] ID: %s vs %s", trigger.ID, workflowExecution.Start) + //log.Printf("[INFO] ID: %s vs %s", trigger.ID, workflowExecution.Start) if trigger.ID == workflowExecution.Start { if trigger.AppName == "User Input" { startFound = true diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 6978dd08..0c673342 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1784,7 +1784,20 @@ const AppCreator = (props) => { }} onBlur={event => { var parsedurl = event.target.value - if (parsedurl.startsWith("curl")) { + if (parsedurl.startsWith("PUT ") || parsedurl.startsWith("GET ") ||parsedurl.startsWith("POST ") || parsedurl.startsWith("DELETE ") ||parsedurl.startsWith("PATCH ") || parsedurl.startsWith("CONNECT ")) { + const tmp = parsedurl.split(" ") + + if (tmp.length > 1) { + parsedurl = tmp[1] + setActionField("url", parsedurl) + setUrlPath(parsedurl) + + setCurrentActionMethod(tmp[0].toUpperCase()) + setActionField("method", tmp[0].toUpperCase()) + } + + setUpdate(Math.random()) + } else if (parsedurl.startsWith("curl")) { const request = parseCurl(event.target.value) if (request !== event.target.value) { if (request.method.toUpperCase() !== currentAction.Method) { @@ -1819,7 +1832,8 @@ const AppCreator = (props) => { } } - if (parsedurl !== undefined) { + console.log("PARSED: ", parsedurl) + if (parsedurl !== undefined) { if (parsedurl.includes("<") && parsedurl.includes(">")) { parsedurl = parsedurl.split("<").join("{") parsedurl = parsedurl.split(">").join("}") @@ -1920,9 +1934,9 @@ const AppCreator = (props) => { Cancel +
+ + {actionAmount > 0 && actionAmount < actions.length ? null : + + } +
diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 430645ef..15289670 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -21,6 +21,7 @@ import {Link} from 'react-router-dom'; import Breadcrumbs from '@material-ui/core/Breadcrumbs'; import ReactJson from 'react-json-view' import Chip from '@material-ui/core/Chip'; +import { useTheme } from '@material-ui/core/styles'; import CachedIcon from '@material-ui/icons/Cached'; import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; @@ -113,6 +114,7 @@ const Apps = (props) => { const { globalUrl, isLoggedIn, isLoaded, userdata } = props; //const [workflows, setWorkflows] = React.useState([]); + const theme = useTheme(); const baseRepository = "https://github.com/frikky/shuffle-apps" const alert = useAlert() const [selectedApp, setSelectedApp] = React.useState({}); @@ -316,10 +318,18 @@ const Apps = (props) => { boxColor = "orange" } + if (data.invalid) { + boxColor = "red" + } + + //
+ //
var imageline = data.large_image.length === 0 ? - {data.title} + {data.title} : - {data.title} + {data.title} { + //console.log("IMG LOADED!: ", event.target) + }} /> // FIXME - add label to apps, as this might be slow with A LOT of apps var newAppname = data.name @@ -341,7 +351,7 @@ const Apps = (props) => { } var description = data.description - const maxDescLen = 56 + const maxDescLen = 51 if (description.length > maxDescLen) { description = data.description.slice(0, maxDescLen)+"..." } @@ -364,8 +374,8 @@ const Apps = (props) => { } } }}> - - + + {imageline}
@@ -520,9 +530,9 @@ const Apps = (props) => { : null var imageline = selectedApp.large_image === undefined || selectedApp.large_image.length === 0 ? - {selectedApp.title} + {selectedApp.title} : - {selectedApp.title} + {selectedApp.title} const GetAppExample = () => { if (selectedAction.returns === undefined) { @@ -639,7 +649,7 @@ const Apps = (props) => { updateAppField(selectedApp.id, "sharing", !selectedApp.sharing) //setSelectedAction(event.target.value) }} - style={{width: 150, backgroundColor: inputColor, color: "white", height: 35, marginleft: 10,}} + style={{width: 150, backgroundColor: theme.palette.surfaceColor, backgroundColor: inputColor, color: "white", height: 35, marginleft: 10,}} SelectDisplayProps={{ style: { marginLeft: 10, From 6ad9a3bfaea72b022877d7d5761fc56c0e69034b Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 27 Jan 2021 15:19:45 +0100 Subject: [PATCH 089/185] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c5bfd74..f9589f40 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ https://shuffler.io -**Shuffle Apps** +[**App magicians**](https://github.com/frikky/shuffle-apps) From f3b45de994d241a071a64fbea1cee8d88d56e4bf Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 31 Jan 2021 10:05:34 +0100 Subject: [PATCH 090/185] Fixed broken conditions link --- .env | 4 ++-- frontend/src/views/AngularWorkflow.jsx | 2 +- frontend/src/views/AppCreator.jsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 45389f1f..0fe788c9 100644 --- a/.env +++ b/.env @@ -29,7 +29,7 @@ BACKEND_PORT=5001 FRONTEND_PORT=3001 FRONTEND_PORT_HTTPS=3443 OUTER_HOSTNAME=shuffle-backend -DB_LOCATION=./shuffle-database +DB_LOCATION=./shuffle-database-demo # Proxy configurations. SHUFFLE_PASS_WORKER_PROXY must be FALSE to not pass the proxy information to sub-apps. # PS: It will skip proxy for @@ -39,7 +39,7 @@ SHUFFLE_PASS_WORKER_PROXY=TRUE SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_NAME=frikky -SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.3" +SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.5" # Used for auto-cleanup of containers. REALLY important at scale. SHUFFLE_CONTAINER_AUTO_CLEANUP=false diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 7ce3669f..f4aca36c 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -4754,7 +4754,7 @@ const AngularWorkflow = (props) => {

Branch: Conditions - {selectedEdgeIndex}

- What are conditions? + What are conditions?
diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 5b4cb70f..5ee0d1ec 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1702,7 +1702,7 @@ const AppCreator = (props) => {
New action
- Learn more about actions + Learn more about actions
Name {

Actions ({actions.length})

Actions are the tasks performed by an app. Read more about actions and apps - here. +
here.
{loopActions}
From 8671f7a31ef504ba5e6ec9fc475f90fa1484f192 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 31 Jan 2021 11:56:42 +0100 Subject: [PATCH 091/185] Removed most debug logs from App SDK --- backend/app_sdk/app_base.py | 44 +++++++++++++++++++++++-------------- backend/app_sdk/build.sh | 2 +- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index e0c9ec6a..495bd503 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -40,10 +40,22 @@ class AppBase: if action_result["status"] == "EXECUTING": action_result["status"] = "FAILURE" + # FIXME: Add cleanup of parameters to not send to frontend here + params = {} + #action = action_result["action"] + #try: + # for item in action["authentication"]: + # for action["parameters"] + # print("AUTH: ", key, value) + # params[item["key"]] = item["value"] + #except KeyError: + # print("No authentication specified!") + # pass + # I wonder if this actually works self.logger.info("Before last stream result") url = "%s%s" % (self.base_url, stream_path) - print("URL: %s" % url) + print("[INFO] URL (URL): %s" % url) try: ret = requests.post(url, headers=headers, json=action_result) self.logger.info("Result: %d" % ret.status_code) @@ -130,7 +142,7 @@ class AppBase: octothorpe_count = param["value"].count(".#") if octothorpe_count > self.result_wrapper_count: self.result_wrapper_count = octothorpe_count - print("NEW OCTOTHORPE WRAPPER: %d" % octothorpe_count) + print("[INFO] NEW OCTOTHORPE WRAPPER: %d" % octothorpe_count) # This whole thing is hard. # item = [{"data": "1.2.3.4", "dataType": "ip"}] @@ -720,7 +732,7 @@ class AppBase: else: tmp = json.loads(parsedlist)[lastsplit[0]] - print(tmp) + #print(tmp) return tmp except IndexError as e: return default_error @@ -751,8 +763,8 @@ class AppBase: # Do stuff here. innervalue = parse_nested_param(data, maxDepth(data)-0) outervalue = parse_nested_param(data, maxDepth(data)-1) - print("INNER: ", innervalue) - print("OUTER: ", outervalue) + #print("INNER: ", innervalue) + #print("OUTER: ", outervalue) if outervalue != innervalue: #print("Outer: ", outervalue, " inner: ", innervalue) @@ -769,7 +781,7 @@ class AppBase: print("Parsed value from %s: %s" % (thistype, parsed_value)) return (parsed_value, True) - print("DATA: %s\n" % data) + #print("DATA: %s\n" % data) return (parse_wrapper(data)[0], True) @@ -829,12 +841,12 @@ class AppBase: return data if len(parsedlist) > 0 and not non_string: - print("Returning parsed list: ", parsedlist) + #print("Returning parsed list: ", parsedlist) return " ".join(parsedlist) elif len(parsedlist) == 1 and non_string: return parsedlist[0] else: - print("Casting back to string because multi: ", parsedlist) + #print("Casting back to string because multi: ", parsedlist) newlist = [] for item in parsedlist: try: @@ -848,13 +860,13 @@ class AppBase: # Parses JSON loops and such down to the item you're looking for def recurse_json(basejson, parsersplit): match = "#(\d+):?-?([0-9a-z]+)?#?" - print("Split: %s\n%s" % (parsersplit, basejson)) + #print("Split: %s\n%s" % (parsersplit, basejson)) try: outercnt = 0 # Loops over split values for value in parsersplit: - print("VALUE: %s\n" % value) + #print("VALUE: %s\n" % value) actualitem = re.findall(match, value, re.MULTILINE) if value == "#": newvalue = [] @@ -875,7 +887,7 @@ class AppBase: return newvalue, True elif len(actualitem) > 0: - print("[INFO] In recursion v2: ", actualitem) + #print("[INFO] In recursion v2: ", actualitem) is_loop = True newvalue = [] @@ -884,7 +896,7 @@ class AppBase: # Means it's a single item -> continue if seconditem == "": - print("[INFO] In first - handling %s" % firstitem) + #print("[INFO] In first - handling %s" % firstitem) tmpitem = basejson[int(firstitem)] try: newvalue, is_loop = recurse_json(tmpitem, parsersplit[outercnt+1:]) @@ -1031,7 +1043,7 @@ class AppBase: baseresult = baseresult.replace(" True,", " true,") baseresult = baseresult.replace(" False", " false,") - print("[INFP] After third parser return - Formatted: ", baseresult) + print("[INFO] After third parser return - Formatted: ", baseresult) basejson = {} try: basejson = json.loads(baseresult) @@ -1685,8 +1697,8 @@ class AppBase: # "id": "body_replacement", #}) - print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") - print("[INFO] Running with params (0): %s" % params) + #print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") + #print("[INFO] Running with params (0): %s" % params) newres = await func(**params) print("[INFO] Returned from execution:", newres) if isinstance(newres, tuple): @@ -1725,7 +1737,7 @@ class AppBase: print("[INFO] POST NEWRES RESULT: ", result) else: - print("[INFO] APP_SDK DONE: Starting MULTI execution (length: %d) with values %s" % (minlength, multi_parameters)) + #print("[INFO] APP_SDK DONE: Starting MULTI execution (length: %d) with values %s" % (minlength, multi_parameters)) # 1. Use number of executions based on the arrays being similar # 2. Find the right value from the parsed multi_params diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 5f09b969..96204598 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.54 +VERSION=0.8.56 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION From 7c0b0c8c2b6f89f5cac3b844f1a69124428f5314 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 1 Feb 2021 01:44:43 +0100 Subject: [PATCH 092/185] Rollback .env --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 0fe788c9..bd8738f6 100644 --- a/.env +++ b/.env @@ -29,7 +29,7 @@ BACKEND_PORT=5001 FRONTEND_PORT=3001 FRONTEND_PORT_HTTPS=3443 OUTER_HOSTNAME=shuffle-backend -DB_LOCATION=./shuffle-database-demo +DB_LOCATION=./shuffle-database # Proxy configurations. SHUFFLE_PASS_WORKER_PROXY must be FALSE to not pass the proxy information to sub-apps. # PS: It will skip proxy for From 11ddf604ec0902da767bc5dd26560575ce3a29f3 Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 3 Feb 2021 03:03:50 +0100 Subject: [PATCH 093/185] Fixed frontend vulnerabilities --- backend/app_sdk/app_base.py | 1 + frontend/package.json | 4 ++-- frontend/src/views/AppCreator.jsx | 4 ++-- frontend/src/views/Apps.jsx | 5 +++++ functions/onprem/worker/worker.go | 4 ++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 495bd503..021f58ed 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1476,6 +1476,7 @@ class AppBase: #json_replacement = tmpitem.replace(actualitem[0][0], replacement, 1) #print("AFTER POST replacement: %s" % json_replacement) + #json_replacement = replacement try: json_replacement = json.loads(replacement) except json.decoder.JSONDecodeError as e: diff --git a/frontend/package.json b/frontend/package.json index bb24de65..0b99f34c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "react-alert": "^5.5.0", "react-alert-template-basic": "^1.0.0", "react-beforeunload": "^2.2.1", - "react-chartjs-2": "^2.8.0", + "react-chartjs-2": "^2.11.1", "react-cookie": "^4.0.1", "react-cytoscapejs": "^1.2.0", "react-device-detect": "^1.9.10", @@ -52,7 +52,7 @@ "react-powerhooks": "0.0.7", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", - "react-scripts": "^3.4.1", + "react-scripts": "^4.0.1", "reactstrap": "^7.1.0", "shellwords": "^0.1.1", "simplebar": "^4.2.3", diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 5ee0d1ec..bc5128e7 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1702,7 +1702,7 @@ const AppCreator = (props) => {
New action
-
Learn more about actions + Learn more about actions
Name {

Actions ({actions.length})

Actions are the tasks performed by an app. Read more about actions and apps - here. + here.
{loopActions}
diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 15289670..9d6263e8 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -168,6 +168,10 @@ const Apps = (props) => { }) function sortByKey(array, key) { + if (array === undefined || array === null) { + return [] + } + return array.sort(function(a, b) { var x = a[key]; var y = b[key]; @@ -222,6 +226,7 @@ const Apps = (props) => { return response.json() }) .then((responseJson) => { + console.log("Apps: ", responseJson) responseJson = sortByKey(responseJson, "large_image") setApps(responseJson) diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 1c36bdb2..e49e313e 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -839,7 +839,7 @@ func shutdown(executionId, workflowId string) { log.Printf("[INFO] Failed abort request: %s", err) } - sleepDuration := 0 + sleepDuration := 1 log.Printf("[INFO] Finished shutdown (after %d seconds).", sleepDuration) // Allows everything to finish in subprocesses time.Sleep(time.Duration(sleepDuration) * time.Second) @@ -2754,7 +2754,7 @@ func main() { if firstRequest { firstRequest = false - workflowExecution.StartedAt = int64(time.Now().Unix()) + //workflowExecution.StartedAt = int64(time.Now().Unix()) cacheKey := fmt.Sprintf("workflowexecution-%s", workflowExecution.ExecutionId) requestCache = cache.New(5*time.Minute, 10*time.Minute) From 87e8a5d549780ecf167f62f60ed787c2ef88f630 Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 3 Feb 2021 03:41:16 +0100 Subject: [PATCH 094/185] Added Code Scanning --- .github/workflows/codeql-analysis.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..727aa8d9 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '38 16 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'go', 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 681be88a3c81fe07eff4e7181d83013d368f5abe Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 6 Feb 2021 13:09:55 +0100 Subject: [PATCH 095/185] Added categories to workflows --- backend/go-app/main.go | 6 +-- backend/go-app/walkoff.go | 68 +++++++++++++++++++++++--- frontend/src/views/AngularWorkflow.jsx | 15 +++--- frontend/src/views/AppCreator.jsx | 20 ++++++-- frontend/src/views/Apps.jsx | 2 +- frontend/src/views/Workflows.jsx | 9 ++-- functions/onprem/orborus/orborus.go | 2 +- 7 files changed, 95 insertions(+), 27 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index b7e9a7fd..1ffd334f 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -2877,8 +2877,8 @@ func fixUserOrg(ctx context.Context, user *User) *User { // Used for testing only. Shouldn't impact production. func handleCors(resp http.ResponseWriter, request *http.Request) bool { - //allowedOrigins := "http://localhost:3000" - allowedOrigins := "http://localhost:3002" + allowedOrigins := "http://localhost:3000" + //allowedOrigins := "http://localhost:3002" resp.Header().Set("Vary", "Origin") resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me, Authorization") @@ -4628,7 +4628,7 @@ func findAvailablePorts(startRange int64, endRange int64) string { func handleSendalert(resp http.ResponseWriter, request *http.Request) { user, err := handleApiAuthentication(resp, request) if err != nil { - log.Printf("Api authentication failed in getworkflows: %s", err) + log.Printf("Api authentication failed in sendalert: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index c77f4345..a23a15a3 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -190,6 +190,10 @@ type WorkflowApp struct { Name string `json:"name" datastore:"name" yaml:"name"` Url string `json:"url" datastore:"url" yaml:"url"` } `json:"contact_info" datastore:"contact_info" yaml:"contact_info" required:false` + ReferenceInfo struct { + DocumentationUrl string `json:"documentation_url" datastore:"documentation_url"` + GithubUrl string `json:"github_url" datastore:"github_url"` + } Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions,noindex"` Authentication Authentication `json:"authentication" yaml:"authentication" required:false datastore:"authentication"` Tags []string `json:"tags" yaml:"tags" required:false datastore:"activated"` @@ -317,6 +321,7 @@ type Action struct { AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` Example string `json:"example,omitempty" datastore:"example"` AuthNotRequired bool `json:"auth_not_required,omitempty" datastore:"auth_not_required" yaml:"auth_not_required"` + Category string `json:"category" datastore:"category"` } // Added environment for location to execute @@ -406,8 +411,26 @@ type Workflow struct { Name string `json:"name" datastore:"name"` Value string `json:"value" datastore:"value,noindex"` } `json:"execution_variables,omitempty" datastore:"execution_variables"` - ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` - PreviouslySaved bool `json:"first_save" datastore:"first_save"` + ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` + PreviouslySaved bool `json:"first_save" datastore:"first_save"` + Categories Categories `json:"categories" datastore:"categories"` +} + +type Category struct { + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + Count int64 `json:"count" datastore:"count"` +} + +type Categories struct { + SIEM Category `json:"siem" datastore:"siem"` + Communication Category `json:"communication" datastore:"communication"` + Assets Category `json:"assets" datastore:"assets"` + Cases Category `json:"cases" datastore:"cases"` + Network Category `json:"network" datastore:"network"` + Intel Category `json:"intel" datastore:"intel"` + EDR Category `json:"edr" datastore:"edr"` + Other Category `json:"other" datastore:"other"` } type ActionResult struct { @@ -1641,13 +1664,16 @@ func getWorkflows(resp http.ResponseWriter, request *http.Request) { q = q.Limit(35) _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { - log.Printf("Failed getting workflows for user %s: %s", user.Username, err) + log.Printf("Failed getting workflows for user %s: %s (0)", user.Username, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return } } else { - log.Printf("Failed getting workflows for user %s: %s", user.Username, err) + log.Printf("Failed getting workflows for user %s: %s (1)", user.Username, err) + //DeleteKey(ctx, "workflow", "5694357e-8063-4580-8529-301cc72df951") + + //log.Printf("Workflows: %#v", workflows) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -2066,6 +2092,32 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add return nil } +func handleCategoryIncrease(workflow Workflow, action Action) Categories { + if action.Category == "" { + log.Printf("Should find app's categories as it's empty during save") + return workflow.Categories + } + + newCategory := "cases" + log.Printf("Adding %s category", newCategory) + switch category := newCategory; { + case category == "cases": + workflow.Categories.Cases.Count += 1 + default: + log.Printf("Can't handle category %s", category) + } + + //Categories Categories `json:"categories" datastore:"categories"` + //found := false + //for _, category := range workflow.Categories { + // if category == newCategory { + // log.Printf("Category %s already exists", category) + // return workflow + //} + //workflow.Categories = handleCategoryIncrease(workflow, action.Category) + return workflow.Categories +} + // Saves a workflow to an ID func saveWorkflow(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) @@ -2166,6 +2218,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME - this shouldn't be necessary with proper API checks newActions := []Action{} allNodes := []string{} + workflow.Categories = Categories{} //log.Printf("Action: %#v", action.Authentication) for _, action := range workflow.Actions { @@ -2193,6 +2246,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { action.Errors = []string{} } + workflow.Categories = handleCategoryIncrease(workflow, action) newActions = append(newActions, action) } @@ -2203,7 +2257,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflowapps, apperr := getAllWorkflowApps(ctx, 500) allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) if err == nil && len(workflowapps) > 0 && apperr == nil { - log.Printf("Setting actions") + //log.Printf("Setting actions") actionFixing := []Action{} appsAdded := []string{} for _, action := range newActions { @@ -2533,7 +2587,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME - might be a sploit to run someone elses app if getAllWorkflowApps // doesn't check sharing=true // Have to do it like this to add the user's apps - log.Println("Apps set starting") + //log.Println("Apps set starting") //log.Printf("EXIT ON ERROR: %#v", workflow.Configuration.ExitOnError) workflowApps := []WorkflowApp{} //memcacheName = "all_apps" @@ -4625,7 +4679,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { user.PrivateApps = privateApps err = setUser(ctx, &user) if err != nil { - log.Printf("[ERROR]Failed removing %s app for user %s: %s", app.Name, user.Username, err) + log.Printf("[ERROR] Failed removing %s app for user %s: %s", app.Name, user.Username, err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": true"}`))) return diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index f4aca36c..598bef39 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -433,7 +433,7 @@ const AngularWorkflow = (props) => { const abortExecution = () => { setExecutionRunning(false) - alert.info("Aborting execution") + //alert.info("Aborting execution") fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/executions/"+executionRequest.execution_id+"/abort", { method: 'GET', headers: { @@ -1859,13 +1859,6 @@ const AngularWorkflow = (props) => { height: "100%", } - const scrollStyle = { - marginTop: 10, - overflow: "scroll", - height: "100%", - overflowX: "auto", - overflowY: "auto", - } const paperAppStyle = { borderRadius: borderRadius, @@ -2429,8 +2422,11 @@ const AngularWorkflow = (props) => { authentication: [], execution_variable: undefined, example: example, + category: app.categories !== null && app.categories !== undefined && app.categories.length > 0 ? app.categories[0] : "" } + // FIXME: overwrite category if the ACTION chosen has a different category + // const image = "url("+app.large_image+")" // FIXME - find the cytoscape offset position @@ -2489,6 +2485,8 @@ const AngularWorkflow = (props) => { } workflow.actions.push(newAppData) + + console.log(workflow.categories) setWorkflow(workflow) if (newAppPopup) { @@ -2605,7 +2603,6 @@ const AngularWorkflow = (props) => { return null } - console.log("APP: ", app) return( ) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index bc5128e7..f23d42ba 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -195,7 +195,7 @@ const AppCreator = (props) => { const alert = useAlert() var upload = "" - const increaseAmount = 30 + const increaseAmount = 50 const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"] const actionBodyRequest = ["POST", "PUT", "PATCH",] const authenticationOptions = ["No authentication", "API key", "Bearer auth", "Basic auth", ] @@ -1230,6 +1230,11 @@ const AppCreator = (props) => { newAction.errors.push("Can't have the same name") actions.push(newAction) + + if (actions.length > actionAmount) { + setActionAmount(actions.length) + } + setActions(actions) setUpdate(Math.random()) } @@ -1539,6 +1544,11 @@ const AppCreator = (props) => { actions[actionIndex] = currentAction } + + if (actions.length > actionAmount) { + setActionAmount(actions.length) + } + setActions(actions) } @@ -1973,6 +1983,7 @@ const AppCreator = (props) => { setUrlPathQueries([]) setUrlPath("") setFileUploadEnabled(false) + }}> Submit @@ -2053,8 +2064,10 @@ const AppCreator = (props) => { setCurrentActionMethod(actionNonBodyRequest[0]) setActionsModalOpen(true) }}>New action + {/* + {actionAmount} {actions.length} {actionAmount > 0 && actionAmount < actions.length ? null : - } + */}
@@ -2136,7 +2150,7 @@ const AppCreator = (props) => {

- {name} + {name} ({actions === null || actions === undefined ? 0 : actions.length})

diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 9d6263e8..5ae1ee01 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -169,7 +169,7 @@ const Apps = (props) => { function sortByKey(array, key) { if (array === undefined || array === null) { - return [] + return array } return array.sort(function(a, b) { diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 83616e2b..f28ad54a 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -191,18 +191,21 @@ const Workflows = (props) => { }) .then((response) => { if (response.status !== 200) { - console.log("Status not 200 for workflows :O!") + console.log("Status not 200 for workflows :O!: ", response.status) + alert.info("Failed getting workflows.") + setWorkflowDone(true) + return } return response.json() }) - .then((responseJson) => { + .then((responseJson) => { setSelectedExecution({}) setWorkflowExecutions([]) if (responseJson !== undefined) { setWorkflows(responseJson) - setWorkflowDone(true) + setWorkflowDone(true) } else { if (isLoggedIn) { alert.error("An error occurred while loading workflows") diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index f4f7f50a..fbfdf0eb 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -258,7 +258,7 @@ func initializeImages() { log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.8.54" + workerVersion = "0.8.56" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } From 43a3b0f1d64e42eaa90ad24efb5741c857b48225 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 6 Feb 2021 13:37:24 +0100 Subject: [PATCH 096/185] Fixed app syncronization issue when making new from workflow --- frontend/src/views/AngularWorkflow.jsx | 30 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 598bef39..9594cce5 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -338,7 +338,7 @@ const AngularWorkflow = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for setting app auth :O!") - } + } return response.json() }) @@ -346,7 +346,10 @@ const AngularWorkflow = (props) => { if (!responseJson.success) { alert.error("Failed to set app auth: "+responseJson.reason) } else { + getAppAuthentication(true) setAuthenticationModalOpen(false) + + // Needs a refresh with the new authentication.. alert.success("Successfully saved new app auth") } }) @@ -930,7 +933,7 @@ const AngularWorkflow = (props) => { "Http", ] - const getAppAuthentication = () => { + const getAppAuthentication = (reset) => { fetch(globalUrl+"/api/v1/apps/authentication", { method: 'GET', headers: { @@ -958,6 +961,10 @@ const AngularWorkflow = (props) => { newauth.push(responseJson.data[key]) } + if (reset === true) { + console.log("APP RESET = reset cy") + cy.on('select', 'node', (e) => onNodeSelect(e, newauth)) + } setAppAuthentication(newauth) } else { alert.error("Failed getting authentications") @@ -992,7 +999,7 @@ const AngularWorkflow = (props) => { //tmpapps = tmpapps.concat(getExtraApps()) //tmpapps = tmpapps.concat(responseJson) setApps(responseJson) - getAppAuthentication() + //getAppAuthentication() setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name))) setPrioritizedApps(responseJson.filter(app => internalIds.includes(app.name))) @@ -1118,7 +1125,7 @@ const AngularWorkflow = (props) => { setSelectedTrigger({}) } - const onNodeSelect = (event) => { + const onNodeSelect = (event, newAppAuth) => { const data = event.target.data() setLastSaved(false) const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id) @@ -1149,7 +1156,8 @@ const AngularWorkflow = (props) => { findAuthId = curaction.authentication_id } - var tmpAuth = JSON.parse(JSON.stringify(appAuthentication)) + var tmpAuth = JSON.parse(JSON.stringify(newAppAuth)) + console.log("Checking authentication: ", tmpAuth) for (var key in tmpAuth) { var item = tmpAuth[key] @@ -1168,6 +1176,7 @@ const AngularWorkflow = (props) => { } curaction.authentication = authenticationOptions + console.log("Authentication: ", authenticationOptions) if (curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "") { curaction.selectedAuthentication = {} } @@ -1562,7 +1571,7 @@ const AngularWorkflow = (props) => { cy.fit(null, 200) - cy.on('select', 'node', (e) => onNodeSelect(e)) + cy.on('select', 'node', (e) => onNodeSelect(e, appAuthentication)) cy.on('select', 'edge', (e) => onEdgeSelect(e)) cy.on('unselect', (e) => onUnselect(e)) @@ -2485,8 +2494,6 @@ const AngularWorkflow = (props) => { } workflow.actions.push(newAppData) - - console.log(workflow.categories) setWorkflow(workflow) if (newAppPopup) { @@ -7075,8 +7082,9 @@ const AngularWorkflow = (props) => { const handleSubmitCheck = () => { console.log("NEW AUTH: ", authenticationOption) if (authenticationOption.label.length === 0) { - alert.info("Label can't be empty") - return + authenticationOption.label = `Auth for ${selectedApp.name}` + //alert.info("Label can't be empty") + //return } for (var key in selectedApp.authentication.parameters) { @@ -7106,7 +7114,6 @@ const AngularWorkflow = (props) => { setNewAppAuth(newAuthOption) //appAuthentication.push(newAuthOption) //setAppAuthentication(appAuthentication) - getAppAuthentication() setUpdate(authenticationOption.id) /* @@ -7141,6 +7148,7 @@ const AngularWorkflow = (props) => { fullWidth color="primary" placeholder={"Auth july 2020"} + defaultValue={`Auth for ${selectedApp.name}`} onChange={(event) => { authenticationOption.label = event.target.value }} From 1760db0bd8b0bb85a64b6fcbd606946a939a5e50 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 6 Feb 2021 17:38:54 +0100 Subject: [PATCH 097/185] Added workflow categories during saving --- backend/go-app/walkoff.go | 45 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index a23a15a3..f8b76e75 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2092,30 +2092,37 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add return nil } -func handleCategoryIncrease(workflow Workflow, action Action) Categories { +// Identifies what a category defined really is +func handleCategoryIncrease(categories Categories, action Action) Categories { + log.Printf("Action: %s, category: %s", action.AppName, action.Category) if action.Category == "" { log.Printf("Should find app's categories as it's empty during save") - return workflow.Categories + return categories } - newCategory := "cases" - log.Printf("Adding %s category", newCategory) - switch category := newCategory; { - case category == "cases": - workflow.Categories.Cases.Count += 1 - default: - log.Printf("Can't handle category %s", category) + // FIXME: Make this an "autodiscover" that's controlled by the category itself + // Should just be a list that's looped against :) + newCategory := strings.ToLower(action.Category) + if strings.Contains(newCategory, "case") || strings.Contains(newCategory, "ticket") || strings.Contains(newCategory, "alert") || strings.Contains(newCategory, "mssp") { + categories.Cases.Count += 1 + } else if strings.Contains(newCategory, "siem") || strings.Contains(newCategory, "event") || strings.Contains(newCategory, "log") || strings.Contains(newCategory, "search") { + categories.SIEM.Count += 1 + } else if strings.Contains(newCategory, "sms") || strings.Contains(newCategory, "comm") || strings.Contains(newCategory, "phone") || strings.Contains(newCategory, "call") || strings.Contains(newCategory, "chat") || strings.Contains(newCategory, "mail") || strings.Contains(newCategory, "phish") { + categories.Communication.Count += 1 + } else if strings.Contains(newCategory, "intel") || strings.Contains(newCategory, "crim") || strings.Contains(newCategory, "ti") { + categories.Intel.Count += 1 + } else if strings.Contains(newCategory, "sand") || strings.Contains(newCategory, "virus") || strings.Contains(newCategory, "malware") || strings.Contains(newCategory, "scan") || strings.Contains(newCategory, "edr") || strings.Contains(newCategory, "endpoint detection") { + // Sandbox lol + categories.EDR.Count += 1 + } else if strings.Contains(newCategory, "vuln") || strings.Contains(newCategory, "fim") || strings.Contains(newCategory, "fim") || strings.Contains(newCategory, "integrity") { + categories.Assets.Count += 1 + } else if strings.Contains(newCategory, "network") || strings.Contains(newCategory, "firewall") || strings.Contains(newCategory, "waf") || strings.Contains(newCategory, "switch") { + categories.Network.Count += 1 + } else { + categories.Other.Count += 1 } - //Categories Categories `json:"categories" datastore:"categories"` - //found := false - //for _, category := range workflow.Categories { - // if category == newCategory { - // log.Printf("Category %s already exists", category) - // return workflow - //} - //workflow.Categories = handleCategoryIncrease(workflow, action.Category) - return workflow.Categories + return categories } // Saves a workflow to an ID @@ -2246,7 +2253,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { action.Errors = []string{} } - workflow.Categories = handleCategoryIncrease(workflow, action) + workflow.Categories = handleCategoryIncrease(workflow.Categories, action) newActions = append(newActions, action) } From 3969c8e998818e63d39da81eb5717c5530f01100 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 8 Feb 2021 08:41:22 +0100 Subject: [PATCH 098/185] #249: Basic fix for sensitive data export --- README.md | 9 ++- backend/app_sdk/app_base.py | 7 +- backend/go-app/app.yaml | 98 -------------------------- backend/go-app/main.go | 4 +- backend/go-app/walkoff.go | 50 ++++++++++--- frontend/src/views/AngularWorkflow.jsx | 60 ++++++++-------- frontend/src/views/AppCreator.jsx | 2 +- frontend/src/views/Apps.jsx | 9 +-- frontend/src/views/Workflows.jsx | 43 ++++++++--- 9 files changed, 122 insertions(+), 160 deletions(-) delete mode 100644 backend/go-app/app.yaml diff --git a/README.md b/README.md index f9589f40..a996646e 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,10 @@ Please consider [sponsoring](https://github.com/sponsors/frikky) the project if ## Support * [Discord](https://discord.gg/B2CBzUm) +* [Twitter](https://twitter.com/shuffleio) * [Email](mailto:frikky@shuffler.io) * [Open issue](https://github.com/frikky/Shuffle/issues/new) +* [Shuffler.io](https://shuffler.io/contact) ## Blogposts * [1. Introducing Shuffle](https://medium.com/security-operation-capybara/introducing-shuffle-an-open-source-soar-platform-part-1-58a529de7d12) @@ -68,12 +70,9 @@ Below is the folder structure with a short explanation ├── README.md # What you're reading right now ├── backend # Contains backend related code. │   ├── go-app # The backend golang webserver -│   ├── app_gen # Code for app generation outside the Shuffle platform │ └── app_sdk # The SDK used for apps -├── frontend # Contains frontend code. ReactJS and cytoscape. Horrible code :) -├── functions # Contains google cloud function code mainly. -│   ├── static_baseline.py # Static code used by stitcher.go to generate code -│   ├── stitcher.go # Attempts to stitch together an app - part of backend now +├── frontend # Contains frontend code. ReactJS, Material UI and cytoscape +├── functions # Has execution and extension resources, such as the Wazuh integration │   ├── onprem # Code for onprem solutions │  │   ├── Orborus # Distributes execution locations │  │   ├── Worker # Runs a workflow diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 021f58ed..5320ecd9 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -62,7 +62,7 @@ class AppBase: if ret.status_code != 200: self.logger.info(ret.text) except requests.exceptions.ConnectionError as e: - self.logger.exception(e) + self.logger.exception("ConnectionError: %s" % e) return except TypeError as e: self.logger.exception(e) @@ -120,6 +120,11 @@ class AppBase: # 1. For the first array, take the total amount(y) (2x3=6) and divide it by the current array (x): 2. x/y = 3. This means do 3 of each value # 2. For the second array, take the total amount(y) (2x3=6) and divide it by the current array (x): 3. x/y = 2. # 3. What does the 3rd array do? Same, but ehhh? + # + # Example4: + # What if there are multiple loops inside a single item? + # + # paramlist = [] listitems = [] diff --git a/backend/go-app/app.yaml b/backend/go-app/app.yaml deleted file mode 100644 index 59f28320..00000000 --- a/backend/go-app/app.yaml +++ /dev/null @@ -1,98 +0,0 @@ -runtime: go111 - -env_variables: - -automatic_scaling: - max_instances: 1 - min_instances: 1 - -handlers: -- url: /api/(.*) - script: auto - secure: always -- url: /static/js/(.*) - static_files: build/static/js/\1 - upload: build/static/js/(.*) - secure: always -- url: /static/css/(.*) - static_files: build/static/css/\1 - upload: build/static/css/(.*) - secure: always -- url: /images/(.*) - static_files: build/images/\1 - upload: build/images/(.*) - secure: always -- url: /(.*\.(json|ico))$ - static_files: build/\1 - upload: build/.*\.(json|ico)$ - secure: always -- url: /manifest.json - static_files: build/manifest.json - upload: build/manifest.json - secure: always - -# lol.. wildcard doesn't work with /api/(.*) for some reason -- url: / - static_files: build/index.html - upload: build/index.html - secure: always -- url: /home - static_files: build/index.html - upload: build/index.html - secure: always -- url: /passwordreset - static_files: build/index.html - upload: build/index.html - secure: always -- url: /login - static_files: build/index.html - upload: build/index.html - secure: always -- url: /register - static_files: build/index.html - upload: build/index.html - secure: always -- url: /workflows - static_files: build/index.html - upload: build/index.html - secure: always -- url: /workflows/(.*) - static_files: build/index.html - upload: build/index.html - secure: always -- url: /info/(.*) - static_files: build/index.html - upload: build/index.html - secure: always -- url: /docs/(.*) - static_files: build/index.html - upload: build/index.html - secure: always -- url: /docs - static_files: build/index.html - upload: build/index.html - secure: always -- url: /settings - static_files: build/index.html - upload: build/index.html - secure: always -- url: /apps - static_files: build/index.html - upload: build/index.html - secure: always -- url: /contact - static_files: build/index.html - upload: build/index.html - secure: always -- url: /apps/(.*) - static_files: build/index.html - upload: build/index.html - secure: always -- url: /register/(.*) - static_files: build/index.html - upload: build/index.html - secure: always -- url: /passwordreset/(.*) - static_files: build/index.html - upload: build/index.html - secure: always diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 1ffd334f..aef237d7 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -6600,7 +6600,9 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { log.Printf("Failed to increase success execution stats: %s", err) } - cacheKey := fmt.Sprintf("workflowapps-sorted") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") requestCache.Delete(cacheKey) resp.WriteHeader(200) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index f8b76e75..91d80d1d 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -414,6 +414,7 @@ type Workflow struct { ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` PreviouslySaved bool `json:"first_save" datastore:"first_save"` Categories Categories `json:"categories" datastore:"categories"` + ExampleArgument string `json:"example_argument" datastore:"example_argument,noindex"` } type Category struct { @@ -2093,13 +2094,26 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add } // Identifies what a category defined really is -func handleCategoryIncrease(categories Categories, action Action) Categories { - log.Printf("Action: %s, category: %s", action.AppName, action.Category) +func handleCategoryIncrease(categories Categories, action Action, workflowapps []WorkflowApp) Categories { if action.Category == "" { - log.Printf("Should find app's categories as it's empty during save") + appName := action.AppName + for _, app := range workflowapps { + if appName != strings.ToLower(app.Name) { + continue + } + + if len(app.Categories) > 0 { + log.Printf("[INFO] Setting category for %s: %s", app.Name, app.Categories) + action.Category = app.Categories[0] + break + } + } + + //log.Printf("Should find app's categories as it's empty during save") return categories } + //log.Printf("Action: %s, category: %s", action.AppName, action.Category) // FIXME: Make this an "autodiscover" that's controlled by the category itself // Should just be a list that's looped against :) newCategory := strings.ToLower(action.Category) @@ -2227,6 +2241,8 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { allNodes := []string{} workflow.Categories = Categories{} + workflowapps, apperr := getAllWorkflowApps(ctx, 500) + //log.Printf("Action: %#v", action.Authentication) for _, action := range workflow.Actions { allNodes = append(allNodes, action.ID) @@ -2253,7 +2269,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { action.Errors = []string{} } - workflow.Categories = handleCategoryIncrease(workflow.Categories, action) + workflow.Categories = handleCategoryIncrease(workflow.Categories, action, workflowapps) newActions = append(newActions, action) } @@ -2261,7 +2277,6 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("[WORKFLOW INIT] NOT PREVIOUSLY SAVED - SET ACTION AUTH!") //AuthenticationId string `json:"authentication_id,omitempty" datastore:"authentication_id"` - workflowapps, apperr := getAllWorkflowApps(ctx, 500) allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) if err == nil && len(workflowapps) > 0 && apperr == nil { //log.Printf("Setting actions") @@ -2819,8 +2834,11 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { Errors: workflow.Errors, } - cacheKey := fmt.Sprintf("workflowapps-sorted") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") + requestCache.Delete(cacheKey) + log.Printf("[INFO] Saved new version of workflow %s (%s) for org %s", workflow.Name, fileId, workflow.OrgId) resp.WriteHeader(200) newBody, err := json.Marshal(returndata) @@ -4706,7 +4724,9 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { if err != nil { log.Printf("Failed to increase total apps loaded stats: %s", err) } - cacheKey := fmt.Sprintf("workflowapps-sorted") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") requestCache.Delete(cacheKey) //err = memcache.Delete(request.Context(), sessionToken) @@ -5273,7 +5293,9 @@ func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { return } - cacheKey := fmt.Sprintf("workflowapps-sorted") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") requestCache.Delete(cacheKey) log.Printf("Changed workflow app %s", app.ID) @@ -6166,7 +6188,9 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, continue } - cacheKey := fmt.Sprintf("workflowapps-sorted") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") requestCache.Delete(cacheKey) } } else { @@ -6680,7 +6704,9 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { } //memcache.Delete(ctx, "all_apps") - cacheKey := fmt.Sprintf("workflowapps-sorted") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") requestCache.Delete(cacheKey) resp.WriteHeader(200) @@ -6824,6 +6850,10 @@ func getAllWorkflowApps(ctx context.Context, maxLen int) ([]WorkflowApp, error) break } + if app.Name == "Shuffle Subflow" { + continue + } + found := false //log.Printf("ACTIONS: %d - %s", len(app.Actions), app.Name) for _, innerapp := range apps { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 9594cce5..99073bc3 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -540,7 +540,7 @@ const AngularWorkflow = (props) => { currentnode.removeClass('awaiting-data-highlight') currentnode.addClass('success-highlight') - if (!visited.includes(item.action.label)) { + if (visited !== undefined && visited !== null && !visited.includes(item.action.label)) { if (executionRunning) { //alert.show("Success in node "+item.action.label) //+" with result "+item.result) @@ -1382,7 +1382,7 @@ const AngularWorkflow = (props) => { //throw BreakException return false } - }); + }) } @@ -1766,13 +1766,13 @@ const AngularWorkflow = (props) => { const stopSchedule = (trigger, triggerindex) => { alert.info("Stopping schedule") fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/schedule/"+trigger.id, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for stream results :O!") @@ -1783,21 +1783,16 @@ const AngularWorkflow = (props) => { .then((responseJson) => { // No matter what, it's being stopped. if (!responseJson.success) { - alert.error("Failed to stop schedule: " + responseJson.reason) - - workflow.triggers[triggerindex].status = "stopped" - trigger.status = "stopped" - setSelectedTrigger(trigger) - setWorkflow(workflow) - saveWorkflow(workflow) + alert.WARNING("Failed to stop schedule: " + responseJson.reason) } else { alert.success("Successfully stopped schedule") - workflow.triggers[triggerindex].status = "stopped" - trigger.status = "stopped" - setSelectedTrigger(trigger) - setWorkflow(workflow) - saveWorkflow(workflow) } + + workflow.triggers[triggerindex].status = "stopped" + trigger.status = "stopped" + setSelectedTrigger(trigger) + setWorkflow(workflow) + saveWorkflow(workflow) }) .catch(error => { alert.error(error.toString()) @@ -2535,7 +2530,8 @@ const AngularWorkflow = (props) => { newAppname = newAppname.slice(0, maxlen)+".." } - const image = "url("+app.large_image+")" + //const image = "url("+app.large_image+")" + const image = app.large_image const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)) const pixelSize = !hover ? "2px" : "4px" newAppStyle.borderLeft = app.is_valid ? `${pixelSize} solid green` : `${pixelSize} solid orange` @@ -2555,7 +2551,7 @@ const AngularWorkflow = (props) => { {setHover(true)}} onMouseOut={() => {setHover(false)}}> -
+ {newAppname} @@ -5530,6 +5526,7 @@ const AngularWorkflow = (props) => { if (trigger.id === undefined) { return } + alert.info("Stopping webhook") fetch(globalUrl+"/api/v1/hooks/"+trigger.id+"/delete", { @@ -5548,18 +5545,23 @@ const AngularWorkflow = (props) => { return response.json() }) .then((responseJson) => { - workflow.triggers[triggerindex].status = "stopped" - trigger.status = "stopped" - setWorkflow(workflow) - setSelectedTrigger(trigger) + if (workflow.triggers[triggerindex] !== undefined) { + workflow.triggers[triggerindex].status = "stopped" + } if (responseJson.success) { //alert.success("Successfully stopped webhook") // Set the status saveWorkflow(workflow) } else { - alert.error("Failed stopping webhook: "+responseJson.reason) + if (responseJson.reason !== undefined) { + alert.error("Failed stopping webhook: "+responseJson.reason) + } } + + trigger.status = "stopped" + setWorkflow(workflow) + setSelectedTrigger(trigger) }) .catch(error => { alert.error(error.toString()) @@ -6937,7 +6939,7 @@ const AngularWorkflow = (props) => { : null const variablesModal = variablesModalOpen ? - { setNewVariableName("") diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index f23d42ba..1a3c1a7c 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2150,7 +2150,7 @@ const AppCreator = (props) => {

- {name} ({actions === null || actions === undefined ? 0 : actions.length}) + {name} {actions === null || actions === undefined || actions.length === 0 ? null : ({actions.length})}

diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 5ae1ee01..2555459b 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -145,6 +145,7 @@ const Apps = (props) => { const [isDropzone, setIsDropzone] = React.useState(false); const upload = React.useRef(null); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" ? true : false + const borderRadius = 3 const { start, stop } = useInterval({ duration: 5000, @@ -330,9 +331,9 @@ const Apps = (props) => { //
//
var imageline = data.large_image.length === 0 ? - {data.title} + {data.title} : - {data.title} { + {data.title} { //console.log("IMG LOADED!: ", event.target) }} /> @@ -535,9 +536,9 @@ const Apps = (props) => { : null var imageline = selectedApp.large_image === undefined || selectedApp.large_image.length === 0 ? - {selectedApp.title} + {selectedApp.title} : - {selectedApp.title} + {selectedApp.title} const GetAppExample = () => { if (selectedAction.returns === undefined) { diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index f28ad54a..0f1d6107 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -395,23 +395,44 @@ const Workflows = (props) => { let exportFileDefaultName = data.name+'.json'; data["owner"] = "" - for (var key in data.triggers) { - const trigger = data.triggers[key] - if (trigger.app_name === "Shuffle Workflow") { - if (trigger.parameters.length > 2) { - trigger.parameters[2].value = "" + if (data.triggers !== null && data.triggers !== undefined) { + for (var key in data.triggers) { + const trigger = data.triggers[key] + if (trigger.app_name === "Shuffle Workflow") { + if (trigger.parameters.length > 2) { + trigger.parameters[2].value = "" + } + } + + if (trigger.status == "running") { + trigger.status = "stopped" } - } - - if (trigger.status == "running") { - trigger.status = "stopped" } } - for (var key in data.actions) { - data.actions[key].authentication_id = "" + if (data.actions !== null && data.actions !== undefined) { + for (var key in data.actions) { + data.actions[key].authentication_id = "" + + for (var subkey in data.actions[key].parameters) { + const param = data.actions[key].parameters[subkey] + if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret")) { + param.value = "" + } + } + } } + if (data.workflow_variables !== null && data.workflow_variables !== undefined) { + for (var key in data.workflow_variables) { + const param = data.workflow_variables[key] + if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret")) { + param.value = "" + } + } + } + + //console.log(data) //return data["org"] = [] From 3ed6d2f2d9e09893819b26f2b834849be1c2ff3c Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 8 Feb 2021 12:01:28 +0100 Subject: [PATCH 099/185] Added gomod for orborus --- frontend/src/views/AngularWorkflow.jsx | 38 +++++---- functions/onprem/orborus/go.mod | 19 +++++ functions/onprem/orborus/go.sum | 112 +++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 functions/onprem/orborus/go.mod create mode 100644 functions/onprem/orborus/go.sum diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 99073bc3..394331aa 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -3841,6 +3841,21 @@ const AngularWorkflow = (props) => { zIndex: 1000, } + const textFieldStyle = { + backgroundColor: inputColor, + borderRadius: borderRadius, + } + + + const innerTextfieldStyle = { + color: "white", + minHeight: 50, + marginLeft: "5px", + maxWidth: "95%", + fontSize: "1em", + borderRadius: borderRadius, + } + const appApiView = Object.getOwnPropertyNames(selectedAction).length > 0 ?
@@ -3894,15 +3909,9 @@ const AngularWorkflow = (props) => { Name { value={selectedActionName} fullWidth onChange={setNewSelectedAction} - style={{backgroundColor: inputColor, color: "white", height: 50}} + style={{backgroundColor: inputColor, color: "white", height: 50, borderRadius: borderRadius,}} SelectDisplayProps={{ style: { marginLeft: 10, maxHeight: 200, + borderRadius: borderRadius, } }} > @@ -6120,15 +6130,9 @@ const AngularWorkflow = (props) => { Date: Mon, 8 Feb 2021 12:20:09 +0100 Subject: [PATCH 100/185] Added changes to install guide --- install-guide.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/install-guide.md b/install-guide.md index 5f5fbd22..e0128b31 100644 --- a/install-guide.md +++ b/install-guide.md @@ -82,8 +82,13 @@ docker-compose up Related issue: #47 -## Local development installation -**Frontend - ReactJS /w cytoscape** +# Local development installation +Local development is pretty straight forward with **ReactJS** and **Golang**. This part is intended to help you run the code for development purposes. + +**PS: You have to stop the Backend Docker container to get this one working** +**PPS: Use the "Launch" branch when developing to get it set up easier** + +## Frontend - ReactJS /w cytoscape http://localhost:3000 - Requires [npm](https://nodejs.org/en/download/)/[yarn](https://yarnpkg.com/lang/en/docs/install/#debian-stable)/your preferred manager. Runs independently from backend. ```bash cd frontend @@ -91,29 +96,32 @@ npm i npm start ``` -**Backend - Golang** +## Backend - Golang http://localhost:5001 - REST API - requires [>=go1.13](https://golang.org/dl/) ```bash export DATASTORE_EMULATOR_HOST=0.0.0.0:8000 cd backend/go-app -go build go run *.go ``` -**Database - Datastore** +**WINDOWS USERS:** You'll have to to add the "export" part as an environment variable. + +## Database - Datastore Based on Google datastore ``` docker run -p 8000:8000 google/cloud-sdk gcloud beta emulators datastore start --project=shuffle --host-port 0.0.0.0:8000 --no-store-on-disk ``` -**Orborus** +## Orborus Execution of Workflows: PS: This requires some specific environment variables ``` cd functions/onprem/orborus go run orborus.go ``` -Environments: + + +Environments (modify for Windows): ``` export ORG_ID=Shuffle export ENVIRONMENT_NAME=Shuffle @@ -122,4 +130,6 @@ export DOCKER_API_VERSION=1.40 export SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY} ``` +**WINDOWS USERS:** You'll have to to add the "export" part as an environment variable. +AND THAT's it - hopefully it worked. If it didn't please email [frikky@shuffler.io](mailto:frikky@shuffler.io) From a6763a99a0a0a75a24e80d6d3d14b8cf3d21a026 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 8 Feb 2021 12:28:33 +0100 Subject: [PATCH 101/185] Removed proxy info --- install-guide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/install-guide.md b/install-guide.md index e0128b31..d1c21169 100644 --- a/install-guide.md +++ b/install-guide.md @@ -127,7 +127,6 @@ export ORG_ID=Shuffle export ENVIRONMENT_NAME=Shuffle export BASE_URL=http://YOUR-IP:5001 export DOCKER_API_VERSION=1.40 -export SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY} ``` **WINDOWS USERS:** You'll have to to add the "export" part as an environment variable. From a52a675a456c0042bff783808d0aafdfedae0276 Mon Sep 17 00:00:00 2001 From: amitk Date: Thu, 11 Feb 2021 22:47:27 +0530 Subject: [PATCH 102/185] https://github.com/frikky/Shuffle/issues/241 - Can't see full result in frontend when not JSON #241 --- frontend/src/views/AngularWorkflow.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 7ce3669f..191a5c81 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -6615,7 +6615,7 @@ const AngularWorkflow = (props) => { } : -
+
Result  {data.result}
From 390630751c07238cedccc1133c30d3f11c8357a9 Mon Sep 17 00:00:00 2001 From: amitk Date: Thu, 11 Feb 2021 23:37:10 +0530 Subject: [PATCH 103/185] Workflow play button not shown as finished when workflow execution is aborted --- frontend/src/views/AngularWorkflow.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 191a5c81..115a4543 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -566,6 +566,9 @@ const AngularWorkflow = (props) => { } break case "FAILURE": + //When status comes as failure, allow user to start workflow execution + setExecutionRunning(false) + currentnode.removeClass('not-executing-highlight') currentnode.removeClass('executing-highlight') currentnode.removeClass('success-highlight') From 83c58849cfa83efd4700acdfdeed007f4f9bb8c8 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 12 Feb 2021 10:08:26 +0100 Subject: [PATCH 104/185] Updates to app grabbing from backend --- backend/go-app/main.go | 4 +-- backend/go-app/walkoff.go | 60 ++++++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index aef237d7..80157f6e 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -2877,8 +2877,8 @@ func fixUserOrg(ctx context.Context, user *User) *User { // Used for testing only. Shouldn't impact production. func handleCors(resp http.ResponseWriter, request *http.Request) bool { - allowedOrigins := "http://localhost:3000" - //allowedOrigins := "http://localhost:3002" + //allowedOrigins := "http://localhost:3000" + allowedOrigins := "http://localhost:3002" resp.Header().Set("Vary", "Origin") resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me, Authorization") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 91d80d1d..8d3b0df6 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -4740,13 +4740,7 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) - if userErr != nil { - log.Printf("Api authentication failed in edit workflow: %s", userErr) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } + ctx := context.Background() location := strings.Split(request.URL.String(), "/") var fileId string @@ -4760,23 +4754,67 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { fileId = location[4] } - ctx := context.Background() app, err := getApp(ctx, fileId) if err != nil { - log.Printf("Error getting app (app config): %s", fileId) + log.Printf("[WARNING] Error getting app %s (app config): %s", fileId, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`)) + return + } + + //if IsValid bool `json:"is_valid" yaml:"is_valid" required:true datastore:"is_valid"` + // Sharing bool `json:"sharing" yaml:"sharing" required:false datastore:"sharing"` + log.Printf("Sharing: %s", app.Sharing) + log.Printf("Generated: %s", app.Generated) + log.Printf("Downloaded: %s", app.Downloaded) + + // FIXME - Handle sharing and such PROPERLY + if app.Sharing && app.Generated { + log.Printf("CAN SHARE APP!") + parsedApi, err := getOpenApiDatastore(ctx, fileId) + if err != nil { + log.Printf("[WARNING] OpenApi doesn't exist for: %s - err: %s", fileId, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if len(parsedApi.ID) > 0 { + parsedApi.Success = true + } else { + parsedApi.Success = false + } + + //log.Printf("PARSEDAPI: %#v", parsedApi) + data, err := json.Marshal(parsedApi) + if err != nil { + log.Printf("[WARNING] Error parsing api json: %s", err) + resp.WriteHeader(422) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed marshalling new parsed swagger: %s"}`, err))) + return + } + + resp.WriteHeader(200) + resp.Write(data) + return + } + + user, userErr := handleApiAuthentication(resp, request) + if userErr != nil { + log.Printf("[WARNING] Api authentication failed in get app: %s", userErr) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return } if user.Id != app.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for app %s", user.Username, app.Name) + log.Printf("[WARNING] Wrong user (%s) for app %s", user.Username, app.Name) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return } - log.Printf("Getting app %s", fileId) + log.Printf("Getting app %s (OpenAPI)", fileId) parsedApi, err := getOpenApiDatastore(ctx, fileId) if err != nil { log.Printf("OpenApi doesn't exist for: %s - err: %s", fileId, err) From 225193e05fec1a6b26ca1a6d6a6043cce29299ca Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 14 Feb 2021 18:24:14 +0100 Subject: [PATCH 105/185] Fixed documentation scrolling issue --- backend/go-app/main.go | 4 +- backend/go-app/walkoff.go | 6 +- frontend/src/App.jsx | 3 + frontend/src/views/AngularWorkflow.jsx | 7 +- frontend/src/views/Docs.jsx | 314 ++++++++++++++----------- 5 files changed, 193 insertions(+), 141 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index f0fbc5c6..1a969309 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -2877,8 +2877,8 @@ func fixUserOrg(ctx context.Context, user *User) *User { // Used for testing only. Shouldn't impact production. func handleCors(resp http.ResponseWriter, request *http.Request) bool { - //allowedOrigins := "http://localhost:3000" - allowedOrigins := "http://localhost:3002" + allowedOrigins := "http://localhost:3000" + //allowedOrigins := "http://localhost:3002" resp.Header().Set("Vary", "Origin") resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me, Authorization") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 4d858dc6..e203b2e8 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -4764,9 +4764,9 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { //if IsValid bool `json:"is_valid" yaml:"is_valid" required:true datastore:"is_valid"` // Sharing bool `json:"sharing" yaml:"sharing" required:false datastore:"sharing"` - log.Printf("Sharing: %s", app.Sharing) - log.Printf("Generated: %s", app.Generated) - log.Printf("Downloaded: %s", app.Downloaded) + //log.Printf("Sharing: %s", app.Sharing) + //log.Printf("Generated: %s", app.Generated) + //log.Printf("Downloaded: %s", app.Downloaded) // FIXME - Handle sharing and such PROPERLY if app.Sharing && app.Generated { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0864b984..18456036 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,6 +30,7 @@ import SettingsPage from "./views/SettingsPage"; import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'; +import ScrollToTop from "./components/ScrollToTop"; import AlertTemplate from "./components/AlertTemplate"; import { positions, Provider } from "react-alert"; @@ -74,6 +75,7 @@ const App = (message, props) => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [dataset, setDataset] = useState(false); const [isLoaded, setIsLoaded] = useState(false); + const [curpath, setCurpath] = useState(typeof window === 'undefined' || window.location === undefined ? "" : window.location.pathname) useEffect(() => { if (dataset === false) { @@ -126,6 +128,7 @@ const App = (message, props) => { } />
:
+
} /> } /> diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 223525c6..a53f403d 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -83,8 +83,8 @@ import cxtmenu from 'cytoscape-cxtmenu'; import { w3cwebsocket as W3CWebSocket } from "websocket"; import { useAlert } from "react-alert"; -import { validateJson } from "./Workflows"; -import { GetParsedPaths } from "./Apps"; +import { validateJson } from "./Workflows.jsx"; +import { GetParsedPaths } from "./Apps.jsx"; const surfaceColor = "#27292D" const inputColor = "#383B40" @@ -428,7 +428,8 @@ const AngularWorkflow = (props) => { handleUpdateResults(responseJson) }) .catch(error => { - alert.error(error.toString()) + console.log("Error: ", error) + //alert.error(error.toString()) stop() }); } diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index e8c4b8b0..76258eaf 100644 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -1,11 +1,16 @@ import React, {useState, useEffect} from 'react'; +import { useTheme } from '@material-ui/core/styles'; import Divider from '@material-ui/core/Divider'; import ReactMarkdown from 'react-markdown'; import {BrowserView, MobileView} from "react-device-detect"; import Button from '@material-ui/core/Button'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; +import Typography from '@material-ui/core/Typography'; +import Paper from '@material-ui/core/Paper'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; import {Link} from 'react-router-dom'; @@ -20,26 +25,21 @@ const Body = { }; const dividerColor = "rgb(225, 228, 232)" - -const SideBar = { - maxWidth: 250, - flex: "1", - position: "fixed", -} - const hrefStyle = { color: "rgba(255, 255, 255, 0.40)", textDecoration: "none" } const Docs = (props) => { - const { isLoaded, globalUrl, inputColor } = props; + const { isLoaded, globalUrl, inputColor, selectedDoc, serverside, isMobile, update} = props; + const theme = useTheme(); const [data, setData] = useState(""); const [firstrequest, setFirstrequest] = useState(true); const [list, setList] = useState([]); const [listLoaded, setListLoaded] = useState(false); const [anchorEl, setAnchorEl] = React.useState(null); + const [baseUrl, setBaseUrl] = React.useState(serverside === true ? "" : window.location.href) function handleClick(event) { setAnchorEl(event.currentTarget); @@ -49,77 +49,21 @@ const Docs = (props) => { setAnchorEl(null); } - useEffect(() => { - if (firstrequest) { - setFirstrequest(false) - fetchDocList() - fetchDocs(props.match.params.key) - return - } + const SidebarPaperStyle = { + backgroundColor: theme.palette.surfaceColor, + overflowX: "hidden", + position: "relative", + padding: 30, + paddingTop: 15, + borderRadius: 5, + } - // Continue this, and find the h2 with the data in it lol - if (window.location.hash.length > 0) { - var parent = document.getElementById("markdown_wrapper") - if (parent !== null) { - var elements = parent.getElementsByTagName('h2') - - const name = window.location.hash.slice(1, window.location.hash.lenth).toLowerCase().split("%20").join(" ").split("_").join(" ").split("-").join(" ") - - console.log(name) - var found = false - for (var key in elements) { - const element = elements[key] - if (element.innerHTML === undefined) { - continue - } - - // Fix location.. - if (element.innerHTML.toLowerCase() === name) { - element.scrollIntoView({behavior: "smooth"}) - found = true - //element.scrollTo({ - // top: element.offsetTop-100, - // behavior: "smooth" - //}) - } - } - - // H# - if (!found) { - var elements = parent.getElementsByTagName('h3') - console.log(name) - var found = false - for (var key in elements) { - const element = elements[key] - if (element.innerHTML === undefined) { - continue - } - - // Fix location.. - if (element.innerHTML.toLowerCase() === name) { - element.scrollIntoView({behavior: "smooth"}) - found = true - //element.scrollTo({ - // top: element.offsetTop-100, - // behavior: "smooth" - //}) - } - } - } - } - //console.log(element) - - //console.log("NAME: ", name) - //console.log(document.body.innerHTML) - // parent = document.getElementById(parent); - - //var descendants = parent.getElementsByTagName(tagname); - - // this.scrollDiv.current.scrollIntoView({ behavior: 'smooth' }); - - //$(".parent").find("h2:contains('Statistics')").parent(); - } - }) + const SideBar = { + maxWidth: 250, + flex: "1", + position: "fixed", + marginTop: 35, + } const fetchDocList = () => { fetch(globalUrl+"/api/v1/docs", { @@ -143,16 +87,17 @@ const Docs = (props) => { const fetchDocs = (docId) => { fetch(globalUrl+"/api/v1/docs/"+docId, { - method: 'GET', + method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, - }) + }) .then((response) => response.json()) - .then((responseJson) => { + .then((responseJson) => { if (responseJson.success) { setData(responseJson.reason) + document.title = "Shuffle "+docId+" documentation" } else { setData("# Error\nThis page doesn't exist.") } @@ -160,13 +105,99 @@ const Docs = (props) => { .catch(error => {}); } + if (firstrequest) { + setFirstrequest(false) + + if (selectedDoc !== undefined) { + setData(selectedDoc.reason) + setList(selectedDoc.list) + setListLoaded(true) + } else { + fetchDocList() + fetchDocs(props.match.params.key) + } + } + + // Handles search-based changes that origin from outside this file + if (serverside !== true && window.location.href !== baseUrl) { + setBaseUrl(window.location.href) + fetchDocs(props.match.params.key) + } + + const parseElementScroll = () => { + var parent = document.getElementById("markdown_wrapper_outer") + if (parent !== null) { + //console.log("IN PARENT") + var elements = parent.getElementsByTagName('h2') + + const name = window.location.hash.slice(1, window.location.hash.lenth).toLowerCase().split("%20").join(" ").split("_").join(" ").split("-").join(" ") + + //console.log(name) + var found = false + for (var key in elements) { + const element = elements[key] + if (element.innerHTML === undefined) { + continue + } + + // Fix location.. + if (element.innerHTML.toLowerCase() === name) { + element.scrollIntoView({behavior: "smooth"}) + found = true + //element.scrollTo({ + // top: element.offsetTop-100, + // behavior: "smooth" + //}) + } + } + + // H# + if (!found) { + var elements = parent.getElementsByTagName('h3') + console.log(name) + var found = false + for (var key in elements) { + const element = elements[key] + if (element.innerHTML === undefined) { + continue + } + + // Fix location.. + if (element.innerHTML.toLowerCase() === name) { + element.scrollIntoView({behavior: "smooth"}) + found = true + //element.scrollTo({ + // top: element.offsetTop-100, + // behavior: "smooth" + //}) + } + } + } + } + //console.log(element) + + //console.log("NAME: ", name) + //console.log(document.body.innerHTML) + // parent = document.getElementById(parent); + + //var descendants = parent.getElementsByTagName(tagname); + + // this.scrollDiv.current.scrollIntoView({ behavior: 'smooth' }); + + //$(".parent").find("h2:contains('Statistics')").parent(); + } + + if (serverside !== true && window.location.hash.length > 0) { + parseElementScroll() + } + const markdownStyle = { color: "rgba(255, 255, 255, 0.65)", flex: "1", - maxWidth: 750, + maxWidth: isMobile ? "100%" : 750, overflow: "hidden", paddingBottom: 200, - marginLeft: 250, + marginLeft: isMobile ? 0 : 275, } function OuterLink(props) { @@ -182,7 +213,7 @@ const Docs = (props) => { function CodeHandler(props) { return ( -
+			
 				
 					{props.value}
 				
@@ -190,13 +221,22 @@ const Docs = (props) => {
 		)
 	}
 
+	function TextWrapper(props) {
+		console.log(props)
+		return (
+			
+				{props.value}			
+			
+		)
+	}
+
 	function Heading(props) {
 		const element = React.createElement(`h${props.level}`, {style: {marginTop: 40}}, props.children)
 		return (
-			
-				{props.level !== 1 ?  : null}
+			
+				{props.level !== 1 ?  : null}
 				{element}
-			
+			
 		)
 	}
 	//React.createElement("p", {style: {color: "red", backgroundColor: "blue"}}, this.props.paragraph)
@@ -213,26 +253,28 @@ const Docs = (props) => {
   const postDataBrowser = 
 		
-
    - {list.map((item, index) => { - const path = "/docs/"+item - const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ") - return ( -
  • - {fetchDocs(item)}}> -

    {newname}

    - -
  • - ) - })} -
+ + + {list.map((item, index) => { + const path = "/docs/"+item + const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ") + return ( +
  • + {fetchDocs(item)}}> + {newname} + +
  • + ) + })} +
    +
    -
    +
    { const mobileStyle = { color: "white", - marginLeft: "15px", - marginRight: "15px", - paddingBottom: "50px", + marginLeft: 15, + marginRight: 15, + paddingBottom: 50, backgroundColor: "inherit", + display: "flex", + flexDirection: "column", } const postDataMobile =
    - - - {list.map(item => { - const path = "/docs/"+item - const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ") - return ( - {window.location.pathname = path}}>{newname} - ) - })} - -
    +
    + + + {list.map((item, index) => { + const path = "/docs/"+item + const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ") + return ( + {window.location.pathname = path}}>{newname} + ) + })} + +
    +
    - @@ -297,7 +348,7 @@ const Docs = (props) => { // {imageModal} - const loadedCheck = isLoaded && listLoaded ? + const loadedCheck =
    {postDataBrowser} @@ -306,9 +357,6 @@ const Docs = (props) => { {postDataMobile}
    - : -
    -
    return (
    From 68f9c541cdbc6d3f68037f910b0adaa74fe44268 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 07:59:43 +0100 Subject: [PATCH 106/185] Bumped docker versions --- backend/app_sdk/app_base.py | 57 ++- backend/go-app/walkoff.go | 9 +- docker-compose.yml | 6 +- frontend/src/views/Admin.jsx | 25 +- functions/onprem/orborus/build.sh | 2 +- functions/onprem/worker/worker.go | 1 - functions/stitcher.go | 703 ------------------------------ 7 files changed, 69 insertions(+), 734 deletions(-) delete mode 100644 functions/stitcher.go diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 5320ecd9..c5e52b09 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -9,7 +9,6 @@ import requests import urllib.parse class AppBase: - """ The base class for Python-based apps in Shuffle, handles logging and callbacks configurations""" __version__ = None app_name = None @@ -21,7 +20,7 @@ class AppBase: # apikey is for the user / org # authorization is for the specific workflow self.url = os.getenv("CALLBACK_URL", "https://shuffler.io") - self.base_url = os.getenv("BASE_URL", "") + self.base_url = os.getenv("BASE_URL", "https://shuffler.io") self.action = os.getenv("ACTION", "") self.authorization = os.getenv("AUTHORIZATION", "") self.current_execution_id = os.getenv("EXECUTIONID", "") @@ -29,7 +28,10 @@ class AppBase: self.result_wrapper_count = 0 if isinstance(self.action, str): - self.action = json.loads(self.action) + try: + self.action = json.loads(self.action) + except: + print("[WARNING] Failed parsing action as JSON") if len(self.base_url) == 0: self.base_url = self.url @@ -530,9 +532,19 @@ class AppBase: "status": "EXECUTING" } + try: + print("Parameters: %d" % len(action["parameters"])) + except KeyError: + action["parameters"] = [] + self.action = copy.deepcopy(action) self.logger.info("ACTION RESULT (start): %s", action_result) + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer %s" % self.authorization + } + if len(self.action) == 0: print("ACTION env not defined") action_result["result"] = "Error in setup ENV: ACTION not defined" @@ -551,10 +563,6 @@ class AppBase: self.send_result(action_result, headers, stream_path) return - headers = { - "Content-Type": "application/json", - "Authorization": "Bearer %s" % self.authorization - } # Add async logger # self.console_logger.handlers[0].stream.set_execution_id() @@ -1363,6 +1371,8 @@ class AppBase: actionname = action["name"] if " " in actionname: actionname.replace(" ", "_", -1) + + #if action.generated: # actionname = actionname.lower() @@ -1919,21 +1929,38 @@ class AppBase: self.send_result(action_result, headers, stream_path) return - - #STOPCOPY - # !!! Let the above line stay - its used for some horrible codegeneration / stitching !!! # - @classmethod - async def run(cls): - """ Connect to Redis and HTTP session, await actions """ + async def run(cls, action=""): logging.basicConfig(format="{asctime} - {name} - {levelname}:{message}", style='{') logger = logging.getLogger(f"{cls.__name__}") logger.setLevel(logging.DEBUG) - print("Started execution!!") - app = cls(redis=None, logger=logger, console_logger=logger) + print("Started execution: %s!!" % cls) + print("Action: %s" % action) + #if isinstance(cls, object): + # self.action = cls # Authorization for the app/function to control the workflow # Function will crash if its wrong, which it probably should. + app = cls(redis=None, logger=logger, console_logger=logger) + + if isinstance(action, object): + app.action = action + + try: + app.authorization = action["authorization"] + app.current_execution_id = action["execution_id"] + except: + pass + + try: + app.url = action["url"] + except: + pass + + try: + app.base_url = action["base_url"] + except: + pass await app.execute_action(app.action) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index e203b2e8..1270d3fb 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2025,11 +2025,10 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { return } - err = increaseStatisticsField(ctx, "total_workflows", fileId, -1, workflow.OrgId) - if err != nil { - log.Printf("Failed to increase total workflows: %s", err) - } - + //err = increaseStatisticsField(ctx, "total_workflows", fileId, -1, workflow.OrgId) + //if err != nil { + // log.Printf("Failed to increase total workflows: %s", err) + //} //memcacheName := fmt.Sprintf("%s_%s", user.Username, fileId) //memcache.Delete(ctx, memcacheName) //memcacheName = fmt.Sprintf("%s_workflows", user.Username) diff --git a/docker-compose.yml b/docker-compose.yml index addef547..a3bc7321 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.56 + image: ghcr.io/frikky/shuffle-frontend:0.8.57 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -45,7 +45,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.5 + image: ghcr.io/frikky/shuffle-orborus:0.8.56 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -54,7 +54,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - SHUFFLE_APP_SDK_VERSION=0.8.51 - - SHUFFLE_WORKER_VERSION=0.8.54 + - SHUFFLE_WORKER_VERSION=0.8.56 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index f590ad60..305db169 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -31,6 +31,7 @@ import { useTheme } from '@material-ui/core/styles'; import HandlePayment from './HandlePayment' import OrgHeader from '../components/OrgHeader' +import CircularProgress from '@material-ui/core/CircularProgress'; import EditIcon from '@material-ui/icons/Edit'; import SelectAllIcon from '@material-ui/icons/SelectAll'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; @@ -1374,12 +1375,24 @@ const Admin = (props) => {
    {selectedOrganization.id === undefined ? -
    - : +
    + + + Loading Organization + +
    + :
    {selectedOrganization.name.length > 0 ? - : null} + : +
    + + + Loading Organization + +
    + } Cloud syncronization What does cloud sync do? Cloud syncronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. @@ -1690,7 +1703,7 @@ const Admin = (props) => { style={{ minWidth: 180, maxWidth: 180 }} /> - {users === undefined ? null : users.map((data, index) => { + {users === undefined || users === null ? null : users.map((data, index) => { var bgColor = "#27292d" if (index % 2 === 0) { bgColor = "#1f2023" @@ -2074,7 +2087,7 @@ const Admin = (props) => { primary="Actions" /> - {authentication === undefined ? null : authentication.map((data, index) => { + {authentication === undefined || authentication === null ? null : authentication.map((data, index) => { var bgColor = "#27292d" if (index % 2 === 0) { bgColor = "#1f2023" @@ -2392,7 +2405,7 @@ const Admin = (props) => { const iconStyle = {marginRight: 10} const data = -
    +
    0 && strings.Contains(items[1], "(AppBase)") { - classname = strings.Split(items[1], "(")[0] - } else { - log.Println("Something wrong :( (horrible programming right here)") - os.Exit(3) - } - } - - if strings.Contains(line, "if __name__ ==") { - break - } - - // asyncio.run(HelloWorld.run(), debug=True) - - newfile = append(newfile, line) - } - - filedata = []byte(strings.Join(newfile, "\n")) - return classname, filedata -} - -// https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang -func Copy(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, in) - if err != nil { - return err - } - return out.Close() -} -func ZipFiles(filename string, files []string) error { - newZipFile, err := os.Create(filename) - if err != nil { - return err - } - defer newZipFile.Close() - - zipWriter := zip.NewWriter(newZipFile) - defer zipWriter.Close() - - // Add files to zip - for _, file := range files { - zipfile, err := os.Open(file) - if err != nil { - return err - } - defer zipfile.Close() - - // Get the file information - info, err := zipfile.Stat() - if err != nil { - return err - } - - header, err := zip.FileInfoHeader(info) - if err != nil { - return err - } - - // Using FileInfoHeader() above only uses the basename of the file. If we want - // to preserve the folder structure we can overwrite this with the full path. - filesplit := strings.Split(file, "/") - if len(filesplit) > 1 { - header.Name = filesplit[len(filesplit)-1] - } else { - header.Name = file - } - - // Change to deflate to gain better compression - // see http://golang.org/pkg/archive/zip/#pkg-constants - header.Method = zip.Deflate - - writer, err := zipWriter.CreateHeader(header) - if err != nil { - return err - } - if _, err = io.Copy(writer, zipfile); err != nil { - return err - } - } - - return nil -} - -func getAppbase(filepath string) []string { - appBase, err := ioutil.ReadFile(filepath) - if err != nil { - log.Printf("Readerror: %s", err) - os.Exit(1) - } - - record := false - validLines := []string{} - for _, line := range strings.Split(string(appBase), "\n") { - if strings.Contains(line, "#STOPCOPY") { - log.Println("Stopping copy") - break - } - - if record { - validLines = append(validLines, line) - } - - if strings.Contains(line, "#STARTCOPY") { - log.Println("Starting copy") - record = true - } - } - - return validLines -} - -// Puts together ./static_baseline.py, onprem/app_sdk_app_base.py and the -// appcode in a generated_app folder based on appname+version -func stitcher(appname string, appversion string) string { - baselinefile := "static_baseline.py" - appfolder := "apps" - appbasefile := "onprem/app_sdk/app_base.py" - - baseline, err := ioutil.ReadFile(baselinefile) - if err != nil { - log.Printf("Readerror: %s", err) - os.Exit(1) - } - - sourceappfile := fmt.Sprintf("%s/%s/%s/src/app.py", appfolder, appname, appversion) - appfile, err := ioutil.ReadFile(sourceappfile) - if err != nil { - log.Printf("App readerror: %s", err) - os.Exit(1) - } - - classname, appfile := formatAppfile(appfile) - if len(classname) == 0 { - log.Println("Failed finding classname in file.") - os.Exit(3) - } - - runner := getRunner(classname) - appBase := getAppbase(appbasefile) - - foldername := fmt.Sprintf("generated_apps/%s_%s", appname, appversion) - err = os.Mkdir(foldername, os.ModePerm) - if err != nil { - log.Println("Failed making temporary app folder. Probably already exists. Remaking") - os.RemoveAll(foldername) - os.MkdirAll(foldername, os.ModePerm) - } - - stitched := []byte(string(baseline) + strings.Join(appBase, "\n") + string(appfile) + string(runner)) - err = ioutil.WriteFile(fmt.Sprintf("%s/main.py", foldername), stitched, os.ModePerm) - if err != nil { - log.Println("Failed writing to stitched: %s", err) - os.Exit(3) - } - - err = Copy(fmt.Sprintf("%s/%s/%s/requirements.txt", appfolder, appname, appversion), fmt.Sprintf("%s/requirements.txt", foldername)) - if err != nil { - log.Println("Failed writing to requirement: %s", err) - os.Exit(3) - } - - log.Printf("Successfully stitched files in %s/main.py", foldername) - // Zip the folder - files := []string{ - fmt.Sprintf("%s/main.py", foldername), - fmt.Sprintf("%s/requirements.txt", foldername), - } - outputfile := fmt.Sprintf("%s.zip", foldername) - - err = ZipFiles(outputfile, files) - if err != nil { - log.Fatal(err) - } - - ctx := context.Background() - - // Creates a client. - client, err := storage.NewClient(ctx) - if err != nil { - log.Printf("Failed to create client: %v", err) - os.Exit(3) - } - - // Create bucket handle - bucket := client.Bucket(bucketName) - - remotePath := fmt.Sprintf("apps/%s_%s.zip", appname, appversion) - err = createFileFromFile(bucket, remotePath, outputfile) - if err != nil { - log.Printf("Failed to upload to bucket: %v", err) - os.Exit(3) - } - - os.Remove(outputfile) - return fmt.Sprintf("gs://%s/apps/%s_%s.zip", bucketName, appname, appversion) -} - -func createFileFromFile(bucket *storage.BucketHandle, remotePath, localPath string) error { - ctx := context.Background() - // [START upload_file] - f, err := os.Open(localPath) - if err != nil { - return err - } - defer f.Close() - - wc := bucket.Object(remotePath).NewWriter(ctx) - if _, err = io.Copy(wc, f); err != nil { - return err - } - if err := wc.Close(); err != nil { - return err - } - // [END upload_file] - return nil -} - -// Deploy to google cloud function :) -func deployFunction(appname, localization, applocation string, environmentVariables map[string]string) error { - ctx := context.Background() - service, err := cloudfunctions.NewService(ctx) - if err != nil { - return err - } - - // ProjectsLocationsListCall - projectsLocationsFunctionsService := cloudfunctions.NewProjectsLocationsFunctionsService(service) - location := fmt.Sprintf("projects/%s/locations/%s", gceProject, localization) - functionName := fmt.Sprintf("%s/functions/%s", location, appname) - - cloudFunction := &cloudfunctions.CloudFunction{ - AvailableMemoryMb: 128, - EntryPoint: "authorization", - EnvironmentVariables: environmentVariables, - HttpsTrigger: &cloudfunctions.HttpsTrigger{}, - MaxInstances: 0, - Name: functionName, - Runtime: "python37", - SourceArchiveUrl: applocation, - } - - //getCall := projectsLocationsFunctionsService.Get(fmt.Sprintf("%s/functions/function-5", location)) - //resp, err := getCall.Do() - - createCall := projectsLocationsFunctionsService.Create(location, cloudFunction) - _, err = createCall.Do() - if err != nil { - log.Println("Failed creating new function. Attempting patch, as it might exist already") - - createCall := projectsLocationsFunctionsService.Patch(fmt.Sprintf("%s/functions/%s", location, appname), cloudFunction) - _, err = createCall.Do() - if err != nil { - log.Println("Failed patching function") - return err - } - - log.Printf("Successfully patched %s to %s", appname, localization) - } else { - log.Printf("Successfully deployed %s to %s", appname, localization) - } - - // FIXME - use response to define the HTTPS entrypoint. It's default to an easy one tho - - return nil -} - -func deployAppCloudFunc(appname string, appversion string) { - _ = os.Mkdir("generated_apps", os.ModePerm) - - apikey := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjYwZjQwNjBlNThkNzVmZDNmNzBiZWZmODhjNzk0YTc3NTMyN2FhMzEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3NodWZmbGVyLmlvL2FwaS92MS93b3JrZmxvd3MvMWQ5ZDhjZTItNTY2ZS00YzNmLThhMzctNWQ2YzdkMjAwMGI1L2V4ZWN1dGUiLCJhenAiOiIxMDMwNzY3ODIwNjE0MjQ2MTg0MjIiLCJlbWFpbCI6InNjaGVkdWxlckBzaHVmZmxlLTI0MTUxNy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NjU1Mjc1NTEsImlhdCI6MTU2NTUyMzk1MSwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAzMDc2NzgyMDYxNDI0NjE4NDIyIn0.r0EDq9fjhf_5CPTiltyfk_L3uYJp577Uy0yYPcCAl2nv50_z_oUtbWGBpQLL8gcj-NGd3g4E52Qur8k6hCMIQweLS6WAb1279vGffEoCNDfkWb3Oy-yJGP1kzwLvqFJqnHLkSWYXNWvSyWnEimW8Rryx_m1BXS5wcA8l4NIr83kS7fPZrTwjnwFSeGSThwk91DVARzapQb8r0GEgOUyHZ1aBXnV98mikzSUt-5xFKe9eMdD22YJAj0Ru-DxAxs5nOqghX4PMRysWjshjOMrlR1piPWxqAmewp8YKZDCQ5gXskpeAFBDoULT971Wsx_NCohnJsFqx1JfPS9ZYMTW2oQ" - fullAppname := fmt.Sprintf("%s-%s", strings.Replace(appname, "_", "-", -1), strings.Replace(appversion, ".", "-", -1)) - locations := []string{"europe-west2"} - - // Deploys the app to all locations - bucketname := stitcher(appname, appversion) - environmentVariables := map[string]string{ - "FUNCTION_APIKEY": apikey, - } - - for _, location := range locations { - err := deployFunction(fullAppname, location, bucketname, environmentVariables) - if err != nil { - log.Printf("Failed to deploy: %s", err) - os.Exit(3) - } - } -} - -func loadYaml(fileLocation string) (WorkflowApp, error) { - action := WorkflowApp{} - - yamlFile, err := ioutil.ReadFile(fileLocation) - if err != nil { - log.Printf("yamlFile.Get err: %s", err) - return WorkflowApp{}, err - } - - //log.Printf(string(yamlFile)) - err = yaml.Unmarshal([]byte(yamlFile), &action) - if err != nil { - return WorkflowApp{}, err - } - - return action, nil -} - -// FIXME - deploy to backend (YAML config) -func deployConfigToBackend(appname string, appversion string) error { - // FIXME - no static path pls - action, err := loadYaml(fmt.Sprintf("apps/%s/%s/api.yaml", appname, appversion)) - if err != nil { - log.Println(err) - return err - } - - action.Sharing = true - - data, err := json.Marshal(action) - if err != nil { - return err - } - - url := "http://localhost:5001/api/v1/workflows/apps" - client := &http.Client{} - req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data)) - if err != nil { - return err - } - - req.Header.Set("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjYwZjQwNjBlNThkNzVmZDNmNzBiZWZmODhjNzk0YTc3NTMyN2FhMzEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3NodWZmbGVyLmlvL2FwaS92MS93b3JrZmxvd3MvMWQ5ZDhjZTItNTY2ZS00YzNmLThhMzctNWQ2YzdkMjAwMGI1L2V4ZWN1dGUiLCJhenAiOiIxMDMwNzY3ODIwNjE0MjQ2MTg0MjIiLCJlbWFpbCI6InNjaGVkdWxlckBzaHVmZmxlLTI0MTUxNy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NjU1Mjc1NTEsImlhdCI6MTU2NTUyMzk1MSwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAzMDc2NzgyMDYxNDI0NjE4NDIyIn0.r0EDq9fjhf_5CPTiltyfk_L3uYJp577Uy0yYPcCAl2nv50_z_oUtbWGBpQLL8gcj-NGd3g4E52Qur8k6hCMIQweLS6WAb1279vGffEoCNDfkWb3Oy-yJGP1kzwLvqFJqnHLkSWYXNWvSyWnEimW8Rryx_m1BXS5wcA8l4NIr83kS7fPZrTwjnwFSeGSThwk91DVARzapQb8r0GEgOUyHZ1aBXnV98mikzSUt-5xFKe9eMdD22YJAj0Ru-DxAxs5nOqghX4PMRysWjshjOMrlR1piPWxqAmewp8YKZDCQ5gXskpeAFBDoULT971Wsx_NCohnJsFqx1JfPS9ZYMTW2oQ") - - ret, err := client.Do(req) - if err != nil { - return err - } - - log.Printf("Status: %s", ret.Status) - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - return err - } - - if ret.StatusCode != 200 { - return errors.New(fmt.Sprintf("Status %s. App probably already exists. Raw:\n%s", ret.Status, string(body))) - } - - log.Println(string(body)) - return nil -} - -func tarDirectory(filecontext string) (io.Reader, error) { - - // Create a filereader - //dockerFileReader, err := os.Open(dockerfile) - //if err != nil { - // return err - //} - - //// Read the actual Dockerfile - //readDockerFile, err := ioutil.ReadAll(dockerFileReader) - //if err != nil { - // return err - //} - - // Make a TAR header for the file - tarHeader := &tar.Header{ - Name: filecontext, - Typeflag: tar.TypeDir, - } - - // Writes the header described for the TAR file - buf := new(bytes.Buffer) - tw := tar.NewWriter(buf) - defer tw.Close() - err := tw.WriteHeader(tarHeader) - if err != nil { - return nil, err - } - - dockerFileTarReader := bytes.NewReader(buf.Bytes()) - return dockerFileTarReader, nil -} - -func tarDir(source string, target string) (*bytes.Reader, error) { - filename := filepath.Base(source) - target = filepath.Join(target, fmt.Sprintf("%s.tar", filename)) - tarfile, err := os.Create(target) - if err != nil { - return nil, err - } - - defer tarfile.Close() - - buf := new(bytes.Buffer) - _ = buf - tarball := tar.NewWriter(tarfile) - defer tarball.Close() - - info, err := os.Stat(source) - if err != nil { - return nil, err - } - - var baseDir string - if info.IsDir() { - baseDir = filepath.Base(source) - } - - _ = filepath.Walk(source, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - header, err := tar.FileInfoHeader(info, info.Name()) - if err != nil { - return err - } - - if baseDir != "" { - header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source)) - } - - if err := tarball.WriteHeader(header); err != nil { - return err - } - - if info.IsDir() { - return nil - } - - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() - _, err = io.Copy(tarball, file) - return nil - }) - - dockerFileTarReader := bytes.NewReader(buf.Bytes()) - return dockerFileTarReader, nil -} - -func buildImage(client *client.Client, tags []string, dockerBuildCtxDir string) error { - dockerBuildContext, err := tarDir(dockerBuildCtxDir, ".") - if err != nil { - log.Printf("Error in taring the docker root folder - %s", err.Error()) - return err - } - - imageBuildResponse, err := client.ImageBuild( - context.Background(), - dockerBuildContext, - types.ImageBuildOptions{ - Dockerfile: "Dockerfile", - PullParent: true, - Remove: true, - Tags: tags, - NetworkMode: "host", - }, - ) - - if err != nil { - return err - } - - // Read the STDOUT from the build process - defer imageBuildResponse.Body.Close() - _, err = io.Copy(os.Stdout, imageBuildResponse.Body) - if err != nil { - return err - } - - return nil -} - -// FIXME - deploy to dockerhub -func deployWorker(appname, appversion string) error { - // Get dockerfile from ./apps/appname/appversion/Dockerfile - client, err := client.NewEnvClient() - if err != nil { - return err - } - - tags := []string{fmt.Sprintf("%s-%s", appname, appversion)} - err = buildImage(client, tags, fmt.Sprintf("./apps/%s/%s", appname, appversion)) - if err != nil { - log.Printf("Build error: %s", err) - return err - } - - return nil -} - -// Deploys all cloud functions. Onprem thooo :( -func deployAll() { - allapps := []string{ - "hoxhunt", - "secureworks", - "servicenow", - "lastline", - "netcraft", - "misp", - "email", - "testing", - "http", - "recordedfuture", - "passivetotal", - "carbon_black", - "thehive", - "cortex", - "splunk", - } - - for _, appname := range allapps { - appversion := "1.0.0" - - err := deployConfigToBackend(appname, appversion) - if err != nil { - log.Printf("Failed uploading config: %s", err) - continue - } - - deployAppCloudFunc(appname, appversion) - } -} - -func main() { - deployAll() - return - - appname := "testing" - appversion := "1.0.0" - - err := deployConfigToBackend(appname, appversion) - if err != nil { - log.Printf("Failed uploading config: %s", err) - os.Exit(1) - } - - deployAppCloudFunc(appname, appversion) - - // FIXME - build and deploy to dockerhub as well :) - // Not able to work in remote directory propely... Even tried making an actual tar and checking it rofl - //err := deployWorker(appname, appversion) - //if err != nil { - // log.Printf("Failed to deploy docker worker: %s", err) - //} -} From 6e9717f8d3c3c98cd9e8504dca088c258dac4616 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 08:00:31 +0100 Subject: [PATCH 107/185] Added frontend autoscroll --- frontend/src/components/ScrollToTop.jsx | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 frontend/src/components/ScrollToTop.jsx diff --git a/frontend/src/components/ScrollToTop.jsx b/frontend/src/components/ScrollToTop.jsx new file mode 100644 index 00000000..6b3c9911 --- /dev/null +++ b/frontend/src/components/ScrollToTop.jsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import { withRouter } from 'react-router-dom'; +import ReactGA from 'react-ga'; + +function ScrollToTop({setCurpath, history }) { + useEffect(() => { + const unlisten = history.listen(() => { + window.scroll({ + top: 0, + left: 0, + behavior: "smooth", + }); + + //ReactGA.event({ + // category: "referral", + // action: "new_user_referral", + // label: "", + //}) + + ReactGA.pageview(window.location.pathname) + setCurpath(window.location.pathname) + }); + return () => { + unlisten(); + } + }, []); + + return (null); +} + +// https://stackoverflow.com/questions/36904185/react-router-scroll-to-top-on-every-transition +export default withRouter(ScrollToTop); From 8fb64da39a4367575a61ceb96c8766c63d99ccc9 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 08:00:54 +0100 Subject: [PATCH 108/185] Added initial lambda function --- functions/extensions/aws-lambda/main.go | 66 +++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 functions/extensions/aws-lambda/main.go diff --git a/functions/extensions/aws-lambda/main.go b/functions/extensions/aws-lambda/main.go new file mode 100644 index 00000000..c78467d6 --- /dev/null +++ b/functions/extensions/aws-lambda/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + //"encoding/json" + //"fmt" + "github.com/aws/aws-lambda-go/lambda" + "net/http" +) + +type LambdaPayload struct { + RequestContext struct { + Elb struct { + TargetGroupArn string `json:"targetGroupArn"` + } `json:"elb"` + } `json:"requestContext"` + HTTPMethod string `json:"httpMethod"` + Path string `json:"path"` + Headers map[string]string `json:"headers"` + QueryStringParameters map[string]string `json:"queryStringParameters"` + Body string `json:"body"` + IsBase64Encoded bool `json:"isBase64Encoded"` +} + +type LambdaResponse struct { + IsBase64Encoded bool `json:"isBase64Encoded"` + StatusCode int `json:"statusCode"` + StatusDescription string `json:"statusDescription"` + Headers struct { + SetCookie string `json:"Set-cookie"` + ContentType string `json:"Content-Type"` + } `json:"headers"` + Body string `json:"body"` +} + +func lambda_handler(ctx context.Context, payload LambdaPayload) (LambdaResponse, error) { + response := &LambdaResponse{} + response.Headers.ContentType = "text/html" + response.StatusCode = http.StatusBadRequest + response.StatusDescription = http.StatusText(http.StatusBadRequest) + if payload.HTTPMethod == http.MethodGet && payload.Path == "/myfavoritecar" { + res := "TEST" + //car := &Car{} + //car.Model = "Corvette" + //car.Color = "Red" + //car.Year = 1999 + //res, err := json.Marshal(car) + //if err != nil { + // fmt.Println(err) + // response.StatusCode = http.StatusInternalServerError + // response.StatusDescription = http.StatusText(http.StatusInternalServerError) + // return *response, err + //} + response.Headers.ContentType = "application/json" + response.Body = string(res) + response.StatusCode = http.StatusOK + response.StatusDescription = http.StatusText(http.StatusOK) + return *response, nil + } else { + return *response, nil + } +} + +func main() { + lambda.Start(lambda_handler) +} From 848d4c3adb54503404e9a4ab3ca6e2c3206c7a53 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 11:30:26 +0100 Subject: [PATCH 109/185] Added re-execution button --- backend/app_sdk/app_base.py | 11 +++++-- backend/go-app/main.go | 14 ++++----- frontend/src/views/AngularWorkflow.jsx | 42 +++++++++++++++++--------- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index c5e52b09..963cfc9f 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -7,6 +7,7 @@ import json import logging import requests import urllib.parse +import http.client class AppBase: __version__ = None @@ -57,17 +58,18 @@ class AppBase: # I wonder if this actually works self.logger.info("Before last stream result") url = "%s%s" % (self.base_url, stream_path) - print("[INFO] URL (URL): %s" % url) + #print("[INFO] URL (URL): %s" % url) try: ret = requests.post(url, headers=headers, json=action_result) self.logger.info("Result: %d" % ret.status_code) if ret.status_code != 200: self.logger.info(ret.text) except requests.exceptions.ConnectionError as e: - self.logger.exception("ConnectionError: %s" % e) + #self.logger.exception("ConnectionError: %s" % e) + self.logger.exception("Expected connectionerror happened") return except TypeError as e: - self.logger.exception(e) + #self.logger.exception(e) action_result["status"] = "FAILURE" action_result["result"] = "POST error: %s" % e self.logger.info("Before typeerror stream result") @@ -75,6 +77,9 @@ class AppBase: self.logger.info("Result: %d" % ret.status_code) if ret.status_code != 200: self.logger.info(ret.text) + except http.client.RemoteDisconnected as e: + self.logger.exception("Expected connectionerror happened") + return async def cartesian_product(self, L): if L: diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 1a969309..349cf5b5 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -6904,19 +6904,19 @@ func remoteOrgJobController(org Org, body []byte) error { ctx := context.Background() if !responseData.Success { - log.Printf("Should stop org job controller") + log.Printf("Should stop org job controller because no success?") - if strings.Contains(responseData.Reason, "Bad apikey") { - log.Printf("Bad apikey. Stopping sync for org?: %s", responseData.Reason) + if strings.Contains(responseData.Reason, "Bad apikey") || strings.Contains(responseData.Reason, "Error getting the organization") { + log.Printf("[WARNING] Remote error; Bad apikey or org error. Stopping sync for org: %s", responseData.Reason) if value, exists := scheduledOrgs[org.Id]; exists { // Looks like this does the trick? Hurr - log.Printf("STOPPING ORG SCHEDULE for: %s", org.Id) + log.Printf("[WARNING] STOPPING ORG SCHEDULE for: %s", org.Id) value.Lock() org, err := getOrg(ctx, org.Id) if err != nil { - log.Printf("Failed finding org %s: %s", org.Id, err) + log.Printf("[WARNING] Failed finding org %s: %s", org.Id, err) return err } @@ -6925,9 +6925,9 @@ func remoteOrgJobController(org Org, body []byte) error { org.CloudSync = false err = setOrg(ctx, *org, org.Id) if err != nil { - log.Printf("Failed setting organization when stopping sync: %s", err) + log.Printf("[WARNING] Failed setting organization when stopping sync: %s", err) } else { - log.Printf("Successfully updated the org to not sync") + log.Printf("[INFO] Successfully STOPPED org cloud sync for %s", org.Id) } return errors.New("Stopped schedule for org locally because of bad apikey.") diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index a53f403d..eeae0a0c 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -782,7 +782,7 @@ const AngularWorkflow = (props) => { return true } - const executeWorkflow = () => { + const executeWorkflow = (executionArgument, startNode) => { if (!lastSaved) { //alert.error("You might have forgotten to save before executing.") console.log("FIXME: Might have forgotten to save before executing.") @@ -804,13 +804,13 @@ const AngularWorkflow = (props) => { curelements[i].addClass("not-executing-highlight") } - if (executionText.length > 0) { - alert.success("Starting execution with an execution argument") + if (executionArgument.length > 0) { + alert.success("Starting execution WITH an execution argument") } else { alert.success("Starting execution") } - const data = {"execution_argument": executionText, "start": workflow.start} + const data = {"execution_argument": executionArgument, "start": startNode} fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/execute", { method: 'POST', headers: { @@ -1870,8 +1870,8 @@ const AngularWorkflow = (props) => { const paperAppStyle = { borderRadius: borderRadius, - minHeight: "100px", - maxHeight: "100px", + minHeight: 100, + maxHeight: 100, minWidth: "100%", maxWidth: "100%", marginTop: "5px", @@ -2557,7 +2557,7 @@ const AngularWorkflow = (props) => { {newAppname} - +

    {newAppname}

    @@ -6121,7 +6121,7 @@ const AngularWorkflow = (props) => { : @@ -6502,13 +6502,31 @@ const AngularWorkflow = (props) => { -

    Executing Workflow

    +
    +

    Executing Workflow

    + + + +
    {executionData.status !== undefined && executionData.status.length > 0 ?
    Status: {executionData.status}
    : null } + {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ? +
    + Source: {executionData.execution_source} +
    + : null + } {executionData.started_at !== undefined ?
    Started: {new Date(executionData.started_at*1000).toISOString()} @@ -6521,12 +6539,6 @@ const AngularWorkflow = (props) => {
    : null } - {executionData.execution_source !== undefined && executionData.execution_source.length > 0 ? -
    - Source: {executionData.execution_source} -
    - : null - } {executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ? parsedExecutionArgument() : null } From e1c39b167069b852c5cf9a17a59b32a1187e72cf Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 11:42:23 +0100 Subject: [PATCH 110/185] Added dropzone to workflow view --- backend/app_sdk/Dockerfile | 2 +- frontend/src/views/Workflows.jsx | 59 +++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/backend/app_sdk/Dockerfile b/backend/app_sdk/Dockerfile index 370c31f8..75ec12c4 100644 --- a/backend/app_sdk/Dockerfile +++ b/backend/app_sdk/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7-alpine as base +FROM python:3.9.1-alpine as base FROM base as builder RUN apk --no-cache add --update alpine-sdk libffi libffi-dev musl-dev openssl-dev diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 0f1d6107..05bc5196 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -29,6 +29,7 @@ import PublishIcon from '@material-ui/icons/Publish'; //import JSONPretty from 'react-json-pretty'; //import JSONPrettyMon from 'react-json-pretty/dist/monikai' import ReactJson from 'react-json-view' +import Dropzone from '../components/Dropzone'; import {Link} from 'react-router-dom'; import { useAlert } from "react-alert"; @@ -108,6 +109,8 @@ const Workflows = (props) => { const [deleteModalOpen, setDeleteModalOpen] = React.useState(false); const [editingWorkflow, setEditingWorkflow] = React.useState({}) const [executionLoading, setExecutionLoading] = React.useState(false) + const [isDropzone, setIsDropzone] = React.useState(false); + const { start, stop } = useInterval({ duration: 5000, startImmediate: false, @@ -180,6 +183,58 @@ const Workflows = (props) => {
    : null + const uploadFile = (e) => { + const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; + const files = isDropzone ? e.dataTransfer.files : e.target.files; + + const reader = new FileReader(); + alert.info("Starting upload. Please wait while we validate the workflows") + + try { + reader.addEventListener('load', (e) => { + var data = e.target.result; + setIsDropzone(false) + try { + data = JSON.parse(reader.result) + } catch (e) { + alert.error("Invalid JSON: "+e) + return + } + + // Initialize the workflow itself + const ret = setNewWorkflow(data.name, data.description, data.tags, {}, false) + .then((response) => { + if (response !== undefined) { + // SET THE FULL THING + data.id = response.id + + // Actually create it + const ret = setNewWorkflow(data.name, data.description, data.tags, data, false) + .then((response) => { + if (response !== undefined) { + alert.success("Successfully imported "+data.name) + } + }) + } + }) + .catch(error => { + alert.error("Import error: "+error.toString()) + }); + }) + } catch (e) { + console.log("Error in dropzone: ", e) + } + + reader.readAsText(files[0]); + } + + useEffect(() => { + if (isDropzone) { + //redirectOpenApi(); + setIsDropzone(false); + } + }, [isDropzone]); + const getAvailableWorkflows = () => { fetch(globalUrl+"/api/v1/workflows", { method: 'GET', @@ -1477,7 +1532,9 @@ const Workflows = (props) => { const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
    - + 1366 ? 1366 : 1200, margin: "auto", padding: 20 }} onDrop={uploadFile}> + + {modalView} {deleteModal} {workflowDownloadModalOpen} From bd6f12c56210cceee7930a126f6d4ddd39d52e1a Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 12:05:29 +0100 Subject: [PATCH 111/185] Added subflow execution exploration button --- backend/go-app/walkoff.go | 10 ++++++++++ frontend/src/views/AngularWorkflow.jsx | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 1270d3fb..4a59d6c6 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -3135,9 +3135,19 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // This one doesn't really matter. log.Printf("[INFO] Running POST execution with body of length %d", len(string(body))) + + if body[0] == 34 && body[len(body)-1] == 34 { + body = body[1 : len(body)-1] + } if len(string(body)) < 50 { + //log.Println(body) + // String in string + //log.Println(body) + + //if string(body)[0] == "\"" && string(body)[string(body) log.Printf("Body: %s", string(body)) } + var execution ExecutionRequest err = json.Unmarshal(body, &execution) if err != nil { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index eeae0a0c..87c7461c 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -378,7 +378,20 @@ const AngularWorkflow = (props) => { if (responseJson.length > 0) { // FIXME: Sort this by time setWorkflowExecutions(responseJson) + + const cursearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search + const tmpView = new URLSearchParams(cursearch).get("execution_id") + if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { + //console.log("SHOW EXECUTION ", tmpView) + const execution = responseJson.find(data => data.execution_id === tmpView) + if (execution !== null && execution !== undefined) { + console.log("EXEC: ", execution) + setExecutionData(execution) + setExecutionModalView(1) + } + } } + //alert.info("Loaded executions") //setWorkflowExecutions(responseJson) }) @@ -6635,7 +6648,11 @@ const AngularWorkflow = (props) => { /> {data.action.app_name === "shuffle-subflow" ? - TBD: Load subexecution result for + {validate.valid && data.action.parameters !== undefined && data.action.parameters !== null ? + See subflow execution + : + "TBD: Load subexecution result for" + } : null } From 6d86207c8f2a41754ab87cacc94040caae4d7132 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 16:27:35 +0100 Subject: [PATCH 112/185] Added saving tracker to workflows --- backend/app_sdk/app_base.py | 2 +- backend/go-app/walkoff.go | 7 +- frontend/src/components/Header.js | 113 +++++++++++-------------- frontend/src/views/AngularWorkflow.jsx | 54 ++++++++++-- 4 files changed, 104 insertions(+), 72 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 963cfc9f..25d63d80 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -538,7 +538,7 @@ class AppBase: } try: - print("Parameters: %d" % len(action["parameters"])) + print(action["parameters"]) except KeyError: action["parameters"] = [] diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 4a59d6c6..80e39e0c 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -3135,7 +3135,10 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // This one doesn't really matter. log.Printf("[INFO] Running POST execution with body of length %d", len(string(body))) - + a + if body[0] == 34 && body[len(body)-1] == 34 { + body = body[1 : len(body)-1] + } if body[0] == 34 && body[len(body)-1] == 34 { body = body[1 : len(body)-1] } @@ -6621,7 +6624,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin if !reservedFound { buildLaterFirst = append(buildLaterFirst, buildLater) } else { - log.Printf("\n\n[WARNING] Skipping build of %s to later\n\n", workflowapp.Name) + log.Printf("[WARNING] Skipping build of %s to later", workflowapp.Name) } } } diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index 11dcf59c..45d3a1e0 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -4,10 +4,13 @@ import {BrowserView, MobileView} from "react-device-detect"; import {Link} from 'react-router-dom'; import List from '@material-ui/core/List'; +import Avatar from '@material-ui/core/Avatar'; +import Menu from '@material-ui/core/Menu'; import ListItem from '@material-ui/core/ListItem'; import MenuItem from '@material-ui/core/MenuItem'; import Select from '@material-ui/core/Select'; import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; import HomeIcon from '@material-ui/icons/Home'; import PolymerIcon from '@material-ui/icons/Polymer'; import AppsIcon from '@material-ui/icons/Apps'; @@ -27,6 +30,7 @@ const Header = props => { const [LoginHoverColor, setLoginHoverColor] = useState(hoverOutColor); const [DocsHoverColor, setDocsHoverColor] = useState(hoverOutColor); const [HelpHoverColor, setHelpHoverColor] = useState(hoverOutColor); + const [anchorEl, setAnchorEl] = React.useState(null); const hrefStyle = { color: hoverOutColor, @@ -102,6 +106,17 @@ const Header = props => { setLoginHoverColor(hoverOutColor) } + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + + // Should be based on some path const logoCheck = !homePage ? null : null @@ -182,74 +197,46 @@ const Header = props => {
    - - -
    - Logout -
    -
    - {logoCheck} - + { + setAnchorEl(event.currentTarget); + }}> + + + { + handleClose() + }} + > + { + event.preventDefault() + handleClose() + }}> - + Settings - - {/* - - - - - - */} + {userdata === undefined || userdata.admin === undefined || userdata.admin === null || !userdata.admin ? null : - + { + event.preventDefault() + handleClose() + }}> - + Admin - + } - {userdata === undefined || userdata.orgs === undefined || userdata.orgs === null || userdata.orgs.length <= 1 ? null : - - - - } - - + { + event.preventDefault() + handleClose() + handleClickLogout() + }}> + Logout + +
    @@ -327,7 +314,7 @@ const Header = props => { // const loadedCheck = -
    +
    {loginTextBrowser} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 87c7461c..861c37f1 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -121,6 +121,8 @@ const AngularWorkflow = (props) => { const [bodyWidth, bodyHeight] = useWindowSize(); const appBarSize = 74 + const headerSize = 60 + var to_be_copied = "" const [cystyle, ] = useState(cytoscapestyle) const [cy, setCy] = React.useState() @@ -155,6 +157,9 @@ const AngularWorkflow = (props) => { const [showSkippedActions, setShowSkippedActions] = React.useState(false) const [lastExecution, setLastExecution] = React.useState("") + // 0 = normal, 1 = just done, 2 = normal + const [savingState, setSavingState] = React.useState(0) + const [selectedResult, setSelectedResult] = React.useState({}) const [codeModalOpen, setCodeModalOpen] = React.useState(false); @@ -646,6 +651,7 @@ const AngularWorkflow = (props) => { const saveWorkflow = (curworkflow) => { var success = false + setSavingState(2) // This might not be the right course of action, but seems logical, as items could be running already // Makes it possible to update with a version in current render @@ -654,7 +660,7 @@ const AngularWorkflow = (props) => { if (curworkflow !== undefined) { useworkflow = curworkflow } else { - alert.info("Saving workflow") + //alert.info("Saving workflow") } var cyelements = cy.elements() @@ -752,6 +758,7 @@ const AngularWorkflow = (props) => { credentials: "include", }) .then((response) => { + setSavingState(0) if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!") } @@ -773,10 +780,15 @@ const AngularWorkflow = (props) => { setWorkflow(workflow) } - alert.success("Successfully saved workflow") + //alert.success("Successfully saved workflow") + setSavingState(1) + setTimeout(() => { + setSavingState(0) + }, 3000); } }) .catch(error => { + setSavingState(0) alert.error(error.toString()) }); @@ -3842,7 +3854,6 @@ const AngularWorkflow = (props) => { }) } - const headerSize = 68 const rightsidebarStyle = { position: "fixed", right: 0, @@ -5159,11 +5170,42 @@ const AngularWorkflow = (props) => { })} } + {/*subworkflow === undefined || subworkflow === null || subworkflow.id === undefined ? null : + + */} {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow}
    - Execution Argument: + Execution Argument
    { /> - From e7541320a14cc8c354e0fbdf6e0501bea3dbec46 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 17:20:51 +0100 Subject: [PATCH 113/185] #256: Added category dropdown to app editor --- backend/app_sdk/app_base.py | 30 +++++---- backend/go-app/main.go | 8 +-- backend/go-app/walkoff.go | 7 +-- frontend/src/views/AngularWorkflow.jsx | 29 +++++---- frontend/src/views/AppCreator.jsx | 84 +++++++++++++++++++------- 5 files changed, 102 insertions(+), 56 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 25d63d80..47bb2aa5 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -541,9 +541,11 @@ class AppBase: print(action["parameters"]) except KeyError: action["parameters"] = [] + except TypeError: + pass self.action = copy.deepcopy(action) - self.logger.info("ACTION RESULT (start): %s", action_result) + self.logger.info("Sending starting action result (EXECUTING)") headers = { "Content-Type": "application/json", @@ -1473,10 +1475,10 @@ class AppBase: except KeyError: pass - print("Return value: %s" % value) + #print("Return value: %s" % value) actionname = action["name"] #print("Multicheck ", actualitem) - print("ITEM LENGTH: %d, Actual item: %s" % (len(actualitem), actualitem)) + #print("ITEM LENGTH: %d, Actual item: %s" % (len(actualitem), actualitem)) if len(actualitem) > 0: multiexecution = True @@ -1636,14 +1638,14 @@ class AppBase: # With this parameter ready, add it to... a greater list of parameters. Rofl print("LENGTH OF ARR: %d" % len(resultarray)) - print("RESULTARRAY: %s" % resultarray) + #print("RESULTARRAY: %s" % resultarray) if resultarray not in multi_execution_lists: multi_execution_lists.append(resultarray) multi_parameters[parameter["name"]] = resultarray else: # Parses things like int(value) - print("Normal parsing (not looping) with data %s" % value) + print("Normal parsing (not looping)")#with data %s" % value) value = parse_wrapper_start(value) if parameter["id"] == "body_replacement": @@ -1663,7 +1665,7 @@ class AppBase: # print("PARAM: %s" % parameter) #if param.id == "body_replacement": - print("POST data value: %s" % value) + #print("POST data value: %s" % value) params[parameter["name"]] = value multi_parameters[parameter["name"]] = value @@ -1719,9 +1721,9 @@ class AppBase: #}) #print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") - #print("[INFO] Running with params (0): %s" % params) + print("[INFO] Running execition\n") newres = await func(**params) - print("[INFO] Returned from execution:", newres) + print("\n[INFO] Returned from execution:", newres) if isinstance(newres, tuple): print("[INFO] Handling return as tuple") # Handles files. @@ -1940,16 +1942,16 @@ class AppBase: logger = logging.getLogger(f"{cls.__name__}") logger.setLevel(logging.DEBUG) - print("Started execution: %s!!" % cls) - print("Action: %s" % action) + #print("Started execution: %s!!" % cls) + #print("Action: %s" % action) #if isinstance(cls, object): # self.action = cls - # Authorization for the app/function to control the workflow - # Function will crash if its wrong, which it probably should. app = cls(redis=None, logger=logger, console_logger=logger) - if isinstance(action, object): + if isinstance(action, str): + print("Normal execution. Action is a string.") + elif isinstance(action, object): app.action = action try: @@ -1967,5 +1969,7 @@ class AppBase: app.base_url = action["base_url"] except: pass + else: + print("ACTION TYPE (unhandled): %s" % type(action)) await app.execute_action(app.action) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 349cf5b5..c27de623 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -6615,7 +6615,7 @@ func healthCheckHandler(resp http.ResponseWriter, request *http.Request) { // Creates osfs from folderpath with a basepath as directory base func createFs(basepath, pathname string) (billy.Filesystem, error) { - log.Printf("base: %s, pathname: %s", basepath, pathname) + log.Printf("[INFO] MemFS base: %s, pathname: %s", basepath, pathname) fs := memfs.New() err := filepath.Walk(pathname, @@ -6676,19 +6676,19 @@ func handleAppHotload(location string, forceUpdate bool) error { log.Printf("Failed memfs creation - probably bad path: %s", err) return errors.New(fmt.Sprintf("Failed to find directory %s", location)) } else { - log.Printf("Memfs creation from %s done", location) + log.Printf("[INFO] Memfs creation from %s done", location) } dir, err := fs.ReadDir("") if err != nil { - log.Printf("Failed reading folder: %s", err) + log.Printf("[WARNING] Failed reading folder: %s", err) return err } //log.Printf("Reading app folder: %#v", dir) _, _, err = iterateAppGithubFolders(fs, dir, "", "", forceUpdate) if err != nil { - log.Printf("Err: %s", err) + log.Printf("[WARNING] Githubfolders error: %s", err) return err } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 80e39e0c..5a241064 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -820,7 +820,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { if len(executionRequests.Data) == 0 { executionRequests.Data = []ExecutionRequest{} } else { - log.Printf("[INFO] Executionrequests: %d", len(executionRequests.Data)) + log.Printf("[INFO] Executionrequests (%s): %d", id, len(executionRequests.Data)) } newjson, err := json.Marshal(executionRequests) @@ -3135,7 +3135,6 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // This one doesn't really matter. log.Printf("[INFO] Running POST execution with body of length %d", len(string(body))) - a if body[0] == 34 && body[len(body)-1] == 34 { body = body[1 : len(body)-1] } @@ -5958,7 +5957,7 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Starting app hotloading") + log.Printf("[INFO] Starting app hotloading") // Just need to be logged in // FIXME - should have some permissions? @@ -5983,7 +5982,7 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Hotloading from %s", location) + log.Printf("[INFO] Hotloading from %s", location) err = handleAppHotload(location, true) if err != nil { log.Printf("Failed app hotload: %s", err) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 861c37f1..4ba479b4 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -137,6 +137,7 @@ const AngularWorkflow = (props) => { const [workflow, setWorkflow] = React.useState({}); const [userSettings, setUserSettings] = React.useState({}); const [subworkflow, setSubworkflow] = React.useState({}); + const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); const [leftViewOpen, setLeftViewOpen] = React.useState(true); const [leftBarSize, setLeftBarSize] = React.useState(350) const [executionText, setExecutionText] = React.useState(""); @@ -783,8 +784,8 @@ const AngularWorkflow = (props) => { //alert.success("Successfully saved workflow") setSavingState(1) setTimeout(() => { - setSavingState(0) - }, 3000); + setSavingState(0) + }, 2000); } }) .catch(error => { @@ -830,9 +831,9 @@ const AngularWorkflow = (props) => { } if (executionArgument.length > 0) { - alert.success("Starting execution WITH an execution argument") + //alert.success("Starting execution WITH an execution argument") } else { - alert.success("Starting execution") + //alert.success("Starting execution") } const data = {"execution_argument": executionArgument, "start": startNode} @@ -5154,6 +5155,7 @@ const AngularWorkflow = (props) => { setUpdate(Math.random()) workflow.triggers[selectedTriggerIndex].parameters[0].value = e.target.value.id setWorkflow(workflow) + setSubworkflowStartnode(e.target.value.start) }} style={{backgroundColor: inputColor, color: "white", height: "50px"}} > @@ -5170,9 +5172,9 @@ const AngularWorkflow = (props) => { })} } - {/*subworkflow === undefined || subworkflow === null || subworkflow.id === undefined ? null : + {/*subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null : { + setNewWorkflowCategories([e.target.value]) + setUpdate("added "+e.target.value) + }} + value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]} + style={{backgroundColor: inputColor, color: "white", height: "50px"}} + > + {categories.map(data => ( + + {data} + + ))} + +

    Tags

    + { + newWorkflowTags.push(chip) + setNewWorkflowTags(newWorkflowTags) + setUpdate("added"+chip) + }} + onDelete={(chip, index) => { + newWorkflowTags.splice(index, 1) + setNewWorkflowTags(newWorkflowTags) + setUpdate("delete "+chip) + }} + />
    const actionView = From e7e50a266b61482d9802cc6e611591ad6f063eb8 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 18:29:35 +0100 Subject: [PATCH 114/185] Added description hover for action params --- frontend/src/views/AngularWorkflow.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 4ba479b4..d9a27e1c 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -3667,6 +3667,7 @@ const AngularWorkflow = (props) => { } tmpitem = tmpitem.charAt(0).toUpperCase()+tmpitem.substring(1) + const description = data.description === undefined ? "" : data.description return (
    @@ -3680,10 +3681,12 @@ const AngularWorkflow = (props) => { }}/> : -
    +
    }
    - {tmpitem} + + {tmpitem} +
    {selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 ? null : From 1c12db12d17311711db4ec6be3bb5dfb92376aa2 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 19:01:46 +0100 Subject: [PATCH 115/185] Fixed app dragging as image --- frontend/src/views/AngularWorkflow.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index d9a27e1c..54eb4504 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2581,7 +2581,7 @@ const AngularWorkflow = (props) => { {setHover(true)}} onMouseOut={() => {setHover(false)}}> - {newAppname} + {newAppname} @@ -3681,7 +3681,7 @@ const AngularWorkflow = (props) => { }}/> : -
    +
    }
    From 5c14a3a202d7971af40dedef473c74caa11d6614 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 18 Feb 2021 19:51:27 +0100 Subject: [PATCH 116/185] Lots of minor fixes --- backend/app_sdk/app_base.py | 8 +-- backend/go-app/codegen.go | 13 ++-- backend/go-app/docker.go | 2 +- backend/go-app/main.go | 8 +-- backend/go-app/walkoff.go | 82 ++++++++++++++++++++++---- frontend/src/views/AngularWorkflow.jsx | 2 +- frontend/src/views/AppCreator.jsx | 18 ++++-- frontend/src/views/Apps.jsx | 2 +- 8 files changed, 103 insertions(+), 32 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 47bb2aa5..ba35c635 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -66,7 +66,7 @@ class AppBase: self.logger.info(ret.text) except requests.exceptions.ConnectionError as e: #self.logger.exception("ConnectionError: %s" % e) - self.logger.exception("Expected connectionerror happened") + self.logger.exception("Expected ConnectionError happened") return except TypeError as e: #self.logger.exception(e) @@ -78,7 +78,7 @@ class AppBase: if ret.status_code != 200: self.logger.info(ret.text) except http.client.RemoteDisconnected as e: - self.logger.exception("Expected connectionerror happened") + self.logger.exception("Expected Remotedisconnect happened") return async def cartesian_product(self, L): @@ -432,7 +432,7 @@ class AppBase: content_path = "/api/v1/files/%s/content?execution_id=%s" % (item, full_execution["execution_id"]) ret2 = requests.get("%s%s" % (self.url, content_path), headers=headers) - print("RET2 (file get): %s" % ret2.text) + print("RET2 (file get) done") if ret2.status_code == 200: tmpdata = ret1.json() returndata = { @@ -1721,7 +1721,7 @@ class AppBase: #}) #print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") - print("[INFO] Running execition\n") + print("[INFO] Running normal execution\n") newres = await func(**params) print("\n[INFO] Returned from execution:", newres) if isinstance(newres, tuple): diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 4b20d2fa..74776e88 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -418,11 +418,14 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet fileBalance, ) - if strings.Contains(functionname, "filescan") { - //log.Printf("FUNCTION: %s", data) - log.Println(data) - log.Printf("Queries: %s", queryString) - } + // Use lowercase when checking + /* + if strings.Contains(functionname, "filter") { + //log.Printf("FUNCTION: %s", data) + log.Println(data) + log.Printf("Queries: %s", queryString) + } + */ //log.Printf(data) return functionname, data diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 0a0b4bae..776dbea0 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -299,7 +299,7 @@ func buildImage(tags []string, dockerfileFolder string) error { return err } - log.Printf("Tags: %s", tags) + log.Printf("[INFO] Docker Tags: %s", tags) dockerfileSplit := strings.Split(dockerfileFolder, "/") // Create a buffer diff --git a/backend/go-app/main.go b/backend/go-app/main.go index c27de623..230f50ae 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -6360,13 +6360,13 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { // FIXME: Check whether it's in use. if user.Id != app.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for app %s when verifying swagger", user.Username, app.Name) + log.Printf("[WARNING] Wrong user (%s) for app %s when verifying swagger", user.Username, app.Name) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return } - log.Printf("EDITING APP WITH ID %s", app.ID) + log.Printf("[INFO] EDITING APP WITH ID %s", app.ID) newmd5 = app.ID } @@ -6455,7 +6455,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { identifier = strings.Replace(identifier, " ", "-", -1) identifier = strings.Replace(identifier, "_", "-", -1) - log.Printf("Successfully parsed %s. Proceeding to docker container", identifier) + log.Printf("[INFO] Successfully parsed %s. Proceeding to docker container", identifier) // Now that the baseline is setup, we need to make it into a cloud function // 1. Upload the API to datastore for use @@ -7517,7 +7517,7 @@ func runInit(ctx context.Context) { } } - log.Printf("Downloading OpenAPI data for search - EXTRA APPS") + log.Printf("[INFO] Downloading OpenAPI data for search - EXTRA APPS") apis := "https://github.com/frikky/security-openapis" // THis gets memory problems hahah diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 5a241064..f3103954 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2062,7 +2062,7 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add // FIXME: Add a way to use !add to remove updateAuth := false if !workflowFound && add { - log.Printf("Adding workflow things to auth!") + log.Printf("[INFO] Adding workflow things to auth!") usageItem := AuthenticationUsage{ WorkflowId: workflowId, Nodes: []string{nodeId}, @@ -2073,14 +2073,14 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add auth.NodeCount += 1 updateAuth = true } else if !nodeFound && add { - log.Printf("Adding node things to auth!") + log.Printf("[INFO] Adding node things to auth!") auth.Usage[workflowIndex].Nodes = append(auth.Usage[workflowIndex].Nodes, nodeId) auth.NodeCount += 1 updateAuth = true } if updateAuth { - log.Printf("Updating auth!") + log.Printf("[INFO] Updating auth!") ctx := context.Background() err := setWorkflowAppAuthDatastore(ctx, auth, auth.Id) if err != nil { @@ -4825,7 +4825,7 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Getting app %s (OpenAPI)", fileId) + log.Printf("[INFO] Getting app %s (OpenAPI)", fileId) parsedApi, err := getOpenApiDatastore(ctx, fileId) if err != nil { log.Printf("OpenApi doesn't exist for: %s - err: %s", fileId, err) @@ -6813,18 +6813,76 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { } // Query for the specifci workflowId - q := datastore.NewQuery("workflowexecution").Filter("workflow_id =", fileId).Order("-started_at").Limit(30) + maxAmount := 30 + q := datastore.NewQuery("workflowexecution").Filter("workflow_id =", fileId).Order("-started_at").Limit(maxAmount) var workflowExecutions []WorkflowExecution _, err = dbclient.GetAll(ctx, q, &workflowExecutions) if err != nil { + if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { - q = datastore.NewQuery("workflowexecution").Filter("workflow_id =", fileId).Order("-started_at").Limit(15) - _, err = dbclient.GetAll(ctx, q, &workflowExecutions) - if err != nil { - log.Printf("Error getting workflowexec (2): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting all workflowexecutions for %s"}`, fileId))) - return + q = datastore.NewQuery("workflowexecution").Filter("workflow_id =", fileId).Order("-started_at").Limit(1) + /* + _, err = dbclient.GetAll(ctx, q, &workflowExecutions) + if err != nil { + log.Printf("Error getting workflowexec (2): %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting all workflowexecutions for %s"}`, fileId))) + return + } + */ + + cursorStr := "" + for { + it := dbclient.Run(ctx, q) + //_, err = it.Next(&app) + for { + var workflowExecution WorkflowExecution + _, err := it.Next(&workflowExecution) + if err != nil { + break + } + + workflowExecutions = append(workflowExecutions, workflowExecution) + } + + //log.Printf("Len: %d", len(workflowExecutions)) + if len(workflowExecutions) > maxAmount { + break + } + + nextCursor, err := it.Cursor() + if err != iterator.Done && err != nil { + if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { + //log.Printf("NEXT!") + nextStr := fmt.Sprintf("%s", nextCursor) + if cursorStr == nextStr { + break + } + + cursorStr = nextStr + + continue + } else { + log.Printf("BREAK: %s", err) + break + } + } + + if err != nil { + log.Printf("Cursorerror: %s", err) + break + } else { + //log.Printf("NEXTCURSOR: %s", nextCursor) + nextStr := fmt.Sprintf("%s", nextCursor) + if cursorStr == nextStr { + break + } + + cursorStr = nextStr + q = q.Start(nextCursor) + //cursorStr = nextCursor + //break + } } } else { log.Printf("Error getting workflowexec: %s", err) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 54eb4504..715f8bb0 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -785,7 +785,7 @@ const AngularWorkflow = (props) => { setSavingState(1) setTimeout(() => { setSavingState(0) - }, 2000); + }, 1500); } }) .catch(error => { diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 0026c5c4..645c77d9 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -926,6 +926,7 @@ const AppCreator = (props) => { //console.log(queryitem) } } + //data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) if (item.paths.length > 0) { for (querykey in item.paths) { @@ -1828,13 +1829,19 @@ const AppCreator = (props) => { }} onBlur={event => { var parsedurl = event.target.value + console.log("URL: ", parsedurl) + if (parsedurl.includes("<") && parsedurl.includes(">")) { + console.log("REPLACE") + parsedurl = parsedurl.replace("<", "{") + parsedurl = parsedurl.replace(">", "}") + } + if (parsedurl.startsWith("PUT ") || parsedurl.startsWith("GET ") ||parsedurl.startsWith("POST ") || parsedurl.startsWith("DELETE ") ||parsedurl.startsWith("PATCH ") || parsedurl.startsWith("CONNECT ")) { const tmp = parsedurl.split(" ") if (tmp.length > 1) { parsedurl = tmp[1] setActionField("url", parsedurl) - setUrlPath(parsedurl) setCurrentActionMethod(tmp[0].toUpperCase()) setActionField("method", tmp[0].toUpperCase()) @@ -1900,12 +1907,15 @@ const AppCreator = (props) => { } // Check URL query && headers - setActionField("url", parsedurl) - setUrlPath(parsedurl) + //setActionField("url", parsedurl) } } } + if (event.target.value !== parsedurl) { + setUrlPath(parsedurl) + setActionField("url", parsedurl) + } //console.log("URL: ", request.url) }} /> @@ -1980,7 +1990,7 @@ const AppCreator = (props) => { + + }
    -
    +
    const loginTextMobile = !isLoggedIn ?
    diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 305db169..735814cf 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { makeStyles } from '@material-ui/styles'; import {Link} from 'react-router-dom'; @@ -25,6 +25,7 @@ import IconButton from '@material-ui/core/IconButton'; import Avatar from '@material-ui/core/Avatar'; import Zoom from '@material-ui/core/Zoom'; import { useAlert } from "react-alert"; +import Dropzone from '../components/Dropzone'; import { Dialog, DialogTitle, DialogActions, DialogContent } from '@material-ui/core'; import { useTheme } from '@material-ui/core/styles'; @@ -33,6 +34,8 @@ import OrgHeader from '../components/OrgHeader' import CircularProgress from '@material-ui/core/CircularProgress'; import EditIcon from '@material-ui/icons/Edit'; +import FileCopyIcon from '@material-ui/icons/FileCopy'; +import PublishIcon from '@material-ui/icons/Publish'; import SelectAllIcon from '@material-ui/icons/SelectAll'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; @@ -62,6 +65,7 @@ const Admin = (props) => { const { globalUrl, userdata } = props; var upload = "" + var to_be_copied = "" const theme = useTheme(); const classes = useStyles(); const [firstRequest, setFirstRequest] = React.useState(true); @@ -93,6 +97,14 @@ const Admin = (props) => { const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false) const [authenticationFields, setAuthenticationFields] = React.useState([]) const [showArchived, setShowArchived] = React.useState(false) + const [isDropzone, setIsDropzone] = React.useState(false); + + useEffect(() => { + if (isDropzone) { + //redirectOpenApi(); + setIsDropzone(false); + } + }, [isDropzone]); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" const getApps = () => { @@ -647,6 +659,67 @@ const Admin = (props) => { }); } + const handleFileUpload = (file_id, file) => { + //console.log("FILE: ", file_id, file) + fetch(`${globalUrl}/api/v1/files/${file_id}/upload`, { + method: 'POST', + credentials: "include", + body: file, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!") + return + } + + return response.json() + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch(error => { + //alert.error(error.toString()) + }); + } + + const handleCreateFile = (filename, file) => { + const data = { + "filename": filename, + "org_id": selectedOrganization.id, + "workflow_id": "global", + } + + fetch(globalUrl + "/api/v1/files/create", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + body: JSON.stringify(data), + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!") + return + } + + return response.json() + }) + .then((responseJson) => { + //console.log("RESP: ", responseJson) + if (responseJson.success) { + handleFileUpload(responseJson.id, file) + } else { + alert.error("Failed to upload file ", filename) + } + }) + .catch(error => { + alert.error(error.toString()) + }); + } + const getFiles = () => { fetch(globalUrl + "/api/v1/files", { method: 'GET', @@ -1778,12 +1851,72 @@ const Admin = (props) => {
    : null + const uploadFiles = (files) => { + for (var key in files) { + try { + const filename = files[key].name + var filedata = new FormData() + filedata.append('shuffle_file', files[key]) + + if (typeof(files[key]) === "object") { + handleCreateFile(filename, filedata) + } + + /* + reader.addEventListener('load', (e) => { + var data = e.target.result; + setIsDropzone(false) + console.log(filename) + console.log(data) + console.log(files[key]) + }) + reader.readAsText(files[key]) + */ + } catch (e) { + console.log("Error in dropzone: ", e) + } + } + + getFiles() + } + + const uploadFile = (e) => { + const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; + const files = isDropzone ? e.dataTransfer.files : e.target.files; + + //const reader = new FileReader(); + //alert.info("Starting fileupload") + uploadFiles(files) + } + const filesView = curTab === 5 ? + 1366 ? 1366 : 1200, margin: "auto", padding: 20 }} onDrop={uploadFile}>

    Files

    Files from Workflows. Learn more
    + + upload = ref} onChange={(event) => { + //const file = event.target.value + //const fileObject = URL.createObjectURL(actualFile) + //setFile(fileObject) + //const files = event.target.files[0] + uploadFiles(event.target.files) + }} /> + @@ -1814,6 +1947,9 @@ const Admin = (props) => { + {files === undefined || files === null ? null : files.map((file, index) => { var bgColor = "#27292d" @@ -1833,13 +1969,19 @@ const Admin = (props) => { /> - - - + {file.workflow_id === "global" ? + + - - + : + + + + + + + + } style={{minWidth: 100, maxWidth: 100, overflow: "hidden"}} /> { - { + { downloadFile(file) }}> - + style={{minWidth: 75, maxWidth: 75, overflow: "hidden"}} @@ -1878,11 +2020,31 @@ const Admin = (props) => { */} + { + const elementName = "copy_element_shuffle" + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + navigator.clipboard.writeText(file.id) + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + alert.info(file.id + "copied to clipboard") + } + }}> + + + /> ) })}
    +
    : null const schedulesView = curTab === 4 ? @@ -2445,6 +2607,11 @@ const Admin = (props) => { {editUserModal} {editAuthenticationModal} {data} +
    ) } From ba7248083710c972e4c5b7b8a842a0da10bc644d Mon Sep 17 00:00:00 2001 From: Dima <65077020+dgabriel123@users.noreply.github.com> Date: Fri, 19 Feb 2021 10:46:13 -0500 Subject: [PATCH 119/185] Create CONTRIBUTING.md --- CONTRIBUTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..58990c2e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +## Contributing to Shuffle + +First off, thank you for contributing to [Shuffle](https://shuffler.io/)! Your talents and your contributions are greatly appreciated. + +### Opening a new issue + +If you find a bug or think of an improvement or fix, please open a [new issue](https://github.com/frikky/Shuffle/issues/new). Outline every step necessary to reproduce the bug. Include screenshots and code examples where applicable. The more thorough you are, the better. + +### Working on an issue + +**Shuffle** uses the [GitHub flow](https://guides.github.com/introduction/flow/index.html). All project changes are made through pull requests. + +If you see an issue that you would like to work on, be sure to leave a quick comment asking if the issue is free to be worked on. + +### License + +All contributions are made under the **GNU Affero General Public License v3.0**. [See the license page](https://github.com/frikky/Shuffle/blob/master/LICENSE) for further details. From f663969d5820caab09db5f51fb5f397054d07d54 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 19 Feb 2021 18:43:22 +0100 Subject: [PATCH 120/185] #223: Added subflow tracking features --- backend/app_sdk/app_base.py | 8 +++++-- backend/go-app/walkoff.go | 33 ++++++++++++++++++++++---- frontend/src/views/AngularWorkflow.jsx | 24 ++++++++++++------- functions/onprem/orborus/orborus.go | 2 +- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 15 ++++++++++-- 6 files changed, 65 insertions(+), 19 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index ba35c635..3c39976a 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -8,6 +8,7 @@ import logging import requests import urllib.parse import http.client +import urllib3 class AppBase: __version__ = None @@ -66,7 +67,7 @@ class AppBase: self.logger.info(ret.text) except requests.exceptions.ConnectionError as e: #self.logger.exception("ConnectionError: %s" % e) - self.logger.exception("Expected ConnectionError happened") + self.logger.info("Expected ConnectionError happened") return except TypeError as e: #self.logger.exception(e) @@ -78,7 +79,10 @@ class AppBase: if ret.status_code != 200: self.logger.info(ret.text) except http.client.RemoteDisconnected as e: - self.logger.exception("Expected Remotedisconnect happened") + self.logger.info("Expected Remotedisconnect happened") + return + except urllib3.exceptions.ProtocolError as e: + self.logger.info("Expected ProtocolError happened") return async def cartesian_product(self, L): diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index f3103954..9efbcd66 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -270,6 +270,7 @@ type WorkflowExecution struct { ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` ExecutionId string `json:"execution_id" datastore:"execution_id"` ExecutionSource string `json:"execution_source" datastore:"execution_source"` + ExecutionParent string `json:"execution_parent" datastore:"execution_parent"` ExecutionOrg string `json:"execution_org" datastore:"execution_org"` WorkflowId string `json:"workflow_id" datastore:"workflow_id"` LastNode string `json:"last_node" datastore:"last_node"` @@ -2410,7 +2411,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Actions = newActions newTriggers := []Trigger{} for _, trigger := range workflow.Triggers { - log.Printf("Trigger %s: %s", trigger.TriggerType, trigger.Status) + log.Printf("[INFO] Trigger %s: %s", trigger.TriggerType, trigger.Status) // Check if it's actually running // FIXME: Do this for other triggers too @@ -3135,12 +3136,34 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // This one doesn't really matter. log.Printf("[INFO] Running POST execution with body of length %d", len(string(body))) - if body[0] == 34 && body[len(body)-1] == 34 { - body = body[1 : len(body)-1] + + if len(body) >= 4 { + if body[0] == 34 && body[len(body)-1] == 34 { + body = body[1 : len(body)-1] + } + if body[0] == 34 && body[len(body)-1] == 34 { + body = body[1 : len(body)-1] + } } - if body[0] == 34 && body[len(body)-1] == 34 { - body = body[1 : len(body)-1] + + //workflowExecution.ExecutionSource = "default" + sourceWorkflow, sourceWorkflowOk := request.URL.Query()["source_workflow"] + if sourceWorkflowOk { + //log.Printf("Got source workflow %s", sourceWorkflow) + workflowExecution.ExecutionSource = sourceWorkflow[0] + } else { + //log.Printf("Did NOT get source workflow") + } + + sourceExecution, sourceExecutionOk := request.URL.Query()["source_execution"] + if sourceExecutionOk { + log.Printf("Got source execution%s", sourceExecution) + workflowExecution.ExecutionParent = sourceExecution[0] + } else { + //log.Printf("Did NOT get source execution") + } + if len(string(body)) < 50 { //log.Println(body) // String in string diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 715f8bb0..59cbd2ca 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -5175,7 +5175,7 @@ const AngularWorkflow = (props) => { })} } - {/*subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null : + {subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null : - */} - {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow} + } + {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow}
    @@ -6400,6 +6400,9 @@ const AngularWorkflow = (props) => { return {"email"} trigger.trigger_type === "EMAIL").large_image} style={{width: size, height: size}} /> } + if (execution.execution_parent !== null && execution.execution_parent !== undefined && execution.execution_parent.length > 0) { + return {"parent trigger.trigger_type === "SUBFLOW").large_image} style={{width: size, height: size}} /> + } return ( {execution.execution_source} @@ -6564,7 +6567,7 @@ const AngularWorkflow = (props) => { - +

    Executing Workflow

    @@ -6580,19 +6583,23 @@ const AngularWorkflow = (props) => {
    {executionData.status !== undefined && executionData.status.length > 0 ?
    - Status: {executionData.status} + Status:   {executionData.status}
    : null } {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ?
    - Source: {executionData.execution_source} + Source:   {executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ? + Parent Workflow + : + executionData.execution_source + }
    : null } {executionData.started_at !== undefined ?
    - Started: {new Date(executionData.started_at*1000).toISOString()} + Started:  {new Date(executionData.started_at*1000).toISOString()}
    : null } @@ -6602,10 +6609,11 @@ const AngularWorkflow = (props) => {
    : null } +
    {executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ? parsedExecutionArgument() : null } - + {executionData.results !== undefined && executionData.results !== null && executionData.results.length > 1 && executionData.results.find(result => result.status === "SKIPPED" || result.status === "FAILURE") ? Date: Fri, 19 Feb 2021 20:07:04 +0100 Subject: [PATCH 121/185] #223: Added subflow in same workflow --- backend/app_sdk/app_base.py | 10 ++-- backend/go-app/walkoff.go | 53 +++++++++++++++++--- frontend/src/views/AngularWorkflow.jsx | 69 ++++++++++++++++++++------ functions/onprem/worker/worker.go | 69 +++++++++++++------------- 4 files changed, 138 insertions(+), 63 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 3c39976a..8c43637d 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -298,7 +298,7 @@ class AppBase: newparams[key] = value[0] has_loop = True else: - print("Key %s is NOT a list within a list: %s" % (key, value)) + print("Key %s is NOT a list within a list" % (key)) newparams[key] = value @@ -1054,7 +1054,7 @@ class AppBase: except KeyError as error: print(f"KeyError in JSON: {error}") - print(f"[INFO] After first trycatch. Baseresult: ", baseresult) + print(f"[INFO] After first trycatch. Baseresult")#, baseresult) # 2. Find the JSON data if len(baseresult) == 0: @@ -1067,7 +1067,7 @@ class AppBase: baseresult = baseresult.replace(" True,", " true,") baseresult = baseresult.replace(" False", " false,") - print("[INFO] After third parser return - Formatted: ", baseresult) + print("[INFO] After third parser return - Formatted")#, baseresult) basejson = {} try: basejson = json.loads(baseresult) @@ -1570,9 +1570,9 @@ class AppBase: multi_parameters[parameter["name"]] = resultarray multi_execution_lists.append(new_replacement) - print("MULTI finished: %s" % json_replacement) + #print("MULTI finished: %s" % json_replacement) else: - print("(2) Pre replacement: %s" % actualitem) + print("(2) Pre replacement. ") #% actualitem) # This is here to handle for loops within variables.. kindof # 1. Find the length of the longest array # 2. Build an array with the base values based on parameter["value"] diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 9efbcd66..7fab3fc6 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1163,6 +1163,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl log.Printf("[WARNING] Actionresult is %s for node %s in %s. Continuing anyway because of workflow configuration.", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) // Finds ALL childnodes to set them to SKIPPED childNodes = findChildNodes(*workflowExecution, actionResult.Action.ID) + // Remove duplicates //log.Printf("CHILD NODES: %d", len(childNodes)) for _, nodeId := range childNodes { @@ -1223,6 +1224,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl Name: curAction.Name, ID: curAction.ID, } + newResult := ActionResult{ Action: newAction, ExecutionId: actionResult.ExecutionId, @@ -3127,6 +3129,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } makeNew := true + start, startok := request.URL.Query()["start"] if request.Method == "POST" { body, err := ioutil.ReadAll(request.Body) if err != nil { @@ -3158,7 +3161,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf sourceExecution, sourceExecutionOk := request.URL.Query()["source_execution"] if sourceExecutionOk { - log.Printf("Got source execution%s", sourceExecution) + //log.Printf("[INFO] Got source execution%s", sourceExecution) workflowExecution.ExecutionParent = sourceExecution[0] } else { //log.Printf("Did NOT get source execution") @@ -3226,12 +3229,11 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // Check for parameters of start and ExecutionId // This is mostly used for user input trigger - start, startok := request.URL.Query()["start"] answer, answerok := request.URL.Query()["answer"] referenceId, referenceok := request.URL.Query()["reference_execution"] if answerok && referenceok { // If answer is false, reference execution with result - log.Printf("Answer is OK AND reference is OK!") + log.Printf("[INFO] Answer is OK AND reference is OK!") if answer[0] == "false" { log.Printf("Should update reference and return, no need for further execution!") @@ -3302,12 +3304,12 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } // Don't override workflow defaults - if startok { - log.Printf("Setting start to %s based on query!", start[0]) - //workflowExecution.Workflow.Start = start[0] - workflowExecution.Start = start[0] - } + } + if startok { + //log.Printf("\n\n[INFO] Setting start to %s based on query!\n\n", start[0]) + //workflowExecution.Workflow.Start = start[0] + workflowExecution.Start = start[0] } // FIXME - regex uuid, and check if already exists? @@ -3495,7 +3497,42 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf break } } + + if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" { + found := false + for _, node := range childNodes { + if node == trigger.ID { + found = true + break + } + } + + if !found { + log.Printf("SHOULD SET TRIGGER %s TO BE SKIPPED", trigger.ID) + + curaction := Action{ + AppName: trigger.AppName, + AppVersion: trigger.AppVersion, + Label: trigger.Label, + Name: trigger.Name, + ID: trigger.ID, + } + + defaultResults = append(defaultResults, ActionResult{ + Action: curaction, + ExecutionId: workflowExecution.ExecutionId, + Authorization: workflowExecution.Authorization, + Result: "Skipped because it's not under the startnode", + StartedAt: 0, + CompletedAt: 0, + Status: "SKIPPED", + }) + } else { + log.Printf("SHOULD KEEP TRIGGER %s", trigger.ID) + } + } } + //childNodes := findChildNodes(workflowExecution, workflowExecution.Start) if !startFound { log.Printf("Startnode %s doesn't exist!", workflowExecution.Start) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 59cbd2ca..2fec90dd 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -259,6 +259,7 @@ const AngularWorkflow = (props) => { setWorkflows(responseJson) if (trigger_index > -1) { + var outersub = {} const trigger = workflow.triggers[trigger_index] if (trigger.parameters.length >= 3) { for (var key in trigger.parameters) { @@ -267,8 +268,23 @@ const AngularWorkflow = (props) => { const sub = responseJson.find(data => data.id === param.value) if (sub !== undefined && subworkflow.id !== sub.id) { setSubworkflow(sub) + outersub = sub } } + + if (param.name === "startnode" && outersub.id !== undefined) { + console.log("SHOULD SET STARTNODE: ", outersub) + const innernode = outersub.actions.find(action => action.id === param.value) + console.log("FOUND NODE: ", innernode) + if (innernode !== undefined && subworkflowStartnode.id !== innernode.id) { + setSubworkflowStartnode(innernode) + } + /* + const sub = responseJson.find(data => data.id === param.value) + setSubworkflow(sub) + } + */ + } } } } @@ -391,7 +407,6 @@ const AngularWorkflow = (props) => { //console.log("SHOW EXECUTION ", tmpView) const execution = responseJson.find(data => data.execution_id === tmpView) if (execution !== null && execution !== undefined) { - console.log("EXEC: ", execution) setExecutionData(execution) setExecutionModalView(1) } @@ -5098,6 +5113,7 @@ const AngularWorkflow = (props) => { workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "workflow", "value": ""} workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "argument", "value": ""} workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "user_apikey", "value": ""} + workflow.triggers[selectedTriggerIndex].parameters[3] = {"name": "startnode", "value": ""} console.log("SETTINGS: ", userSettings) if (userSettings !== undefined && userSettings !== null && userSettings.apikey !== null && userSettings.apikey !== undefined && userSettings.apikey.length > 0) { @@ -5159,22 +5175,41 @@ const AngularWorkflow = (props) => { workflow.triggers[selectedTriggerIndex].parameters[0].value = e.target.value.id setWorkflow(workflow) setSubworkflowStartnode(e.target.value.start) + + // Sets the startnode + if (e.target.value.id !== workflow.id) { + const startnode = e.target.value.actions.find(action => action.id === e.target.value.start) + if (startnode !== undefined && startnode !== null) { + setSubworkflowStartnode(startnode) + } + console.log("STARTNODE: ", startnode) + } }} style={{backgroundColor: inputColor, color: "white", height: "50px"}} > {workflows.map((data, index) => { + /* if (data.id === workflow.id) { return null } + */ return ( - + {data.name} ) })} } + {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow} + +
    +
    +
    + Select the Startnode +
    +
    {subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null : } - {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow}
    diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 123b2631..e09ca874 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -1114,16 +1114,16 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { if isSkipped { //log.Printf("Skipping %s as all parents are done", item.Action.Label) if !arrayContains(visited, item.Action.ID) { - log.Printf("Adding visited (1): %s", item.Action.Label) + log.Printf("[INFO] Adding visited (1): %s", item.Action.Label) visited = append(visited, item.Action.ID) } } else { - log.Printf("Continuing %s as all parents are NOT done", item.Action.Label) + log.Printf("[INFO] Continuing %s as all parents are NOT done", item.Action.Label) appendActions = append(appendActions, item.Action.ID) } } else { if item.Status == "FINISHED" { - log.Printf("Adding visited (2): %s", item.Action.Label) + log.Printf("[INFO] Adding visited (2): %s", item.Action.Label) visited = append(visited, item.Action.ID) } } @@ -1149,7 +1149,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // care if it gets stuck in a loop. // FIXME: Force killing a worker should result in a notification somewhere if len(nextActions) == 0 { - log.Printf("No next action. Finished? Result vs Actions: %d - %d", len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) + log.Printf("[INFO] No next action. Finished? Result vs Actions: %d - %d", len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) exit := true for _, item := range workflowExecution.Results { if item.Status == "EXECUTING" { @@ -1235,6 +1235,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // IF NOT VISITED && IN toExecuteOnPrem // SKIP if it's not onprem toRemove := []int{} + log.Printf("\n\nNEXTACTIONS: %#v\n\n", nextActions) for index, nextAction := range nextActions { action := getAction(workflowExecution, nextAction, environment) // check visited and onprem @@ -1377,7 +1378,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { } if continueOuter { - log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", ")) + log.Printf("[INFO] Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", ")) //for _, tmpaction := range parents[nextAction] { // action := getAction(workflowExecution, tmpaction) // _ = action @@ -1390,10 +1391,10 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // get action status actionResult := getResult(workflowExecution, nextAction) if actionResult.Action.ID == action.ID { - log.Printf("%s already has status %s.", action.ID, actionResult.Status) + log.Printf("[INFO] %s already has status %s.", action.ID, actionResult.Status) continue } else { - log.Printf("%s:%s has no status result yet. Should execute.", action.Name, action.ID) + log.Printf("[INFO] %s:%s has no status result yet. Should execute.", action.Name, action.ID) } appname := action.AppName @@ -1445,7 +1446,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { } // marshal action and put it in there rofl - log.Printf("Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters)) + log.Printf("[INFO] Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters)) actionData, err := json.Marshal(action) if err != nil { @@ -1468,7 +1469,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // Sending full execution so that it won't have to load in every app // This might be an issue if they can read environments, but that's alright // if everything is generated during execution - log.Printf("Deployed with CALLBACK_URL %s and BASE_URL %s", appCallbackUrl, baseUrl) + log.Printf("[INFO] Deployed with CALLBACK_URL %s and BASE_URL %s", appCallbackUrl, baseUrl) env := []string{ fmt.Sprintf("ACTION=%s", string(actionData)), fmt.Sprintf("EXECUTIONID=%s", workflowExecution.ExecutionId), @@ -1593,7 +1594,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { } } - log.Printf("Adding visited (3): %s", action.Label) + log.Printf("[INFO] Adding visited (3): %s", action.Label) visited = append(visited, action.ID) executed = append(executed, action.ID) @@ -1636,14 +1637,22 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { func executionInit(workflowExecution WorkflowExecution) error { parents = map[string][]string{} children = map[string][]string{} - triggersHandled := []string{} startAction = workflowExecution.Start + log.Printf("[INFO] STARTACTION: %s", startAction) if len(startAction) == 0 { - log.Printf("Didn't find execution start action. Setting it to workflow start action.") + log.Printf("[INFO] Didn't find execution start action. Setting it to workflow start action.") startAction = workflowExecution.Workflow.Start } + // Setting up extra counter + for _, trigger := range workflowExecution.Workflow.Triggers { + //log.Printf("Appname trigger (0): %s", trigger.AppName) + if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" { + extra += 1 + } + } + nextActions = append(nextActions, startAction) for _, branch := range workflowExecution.Workflow.Branches { // Check what the parent is first. If it's trigger - skip @@ -1662,27 +1671,15 @@ func executionInit(workflowExecution WorkflowExecution) error { for _, trigger := range workflowExecution.Workflow.Triggers { //log.Printf("Appname trigger (0): %s", trigger.AppName) if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" { - //log.Printf("%s is a special trigger. Checking where.", trigger.AppName) - - found := false - for _, check := range triggersHandled { - if check == trigger.ID { - found = true - break - } - } - - if !found { - extra += 1 - } else { - triggersHandled = append(triggersHandled, trigger.ID) + if branch.SourceID == "c9560766-3f85-4589-8324-311acd6be820" { + log.Printf("BRANCH: %#v", branch) } if trigger.ID == branch.SourceID { - log.Printf("Trigger %s is the source!", trigger.AppName) + log.Printf("[INFO] Trigger %s is the source!", trigger.AppName) sourceFound = true } else if trigger.ID == branch.DestinationID { - log.Printf("Trigger %s is the destination!", trigger.AppName) + log.Printf("[INFO] Trigger %s is the destination!", trigger.AppName) destinationFound = true } } @@ -1691,17 +1688,21 @@ func executionInit(workflowExecution WorkflowExecution) error { if sourceFound { parents[branch.DestinationID] = append(parents[branch.DestinationID], branch.SourceID) } else { - log.Printf("ID %s was not found in actions! Skipping parent. (TRIGGER?)", branch.SourceID) + log.Printf("[INFO] ID %s was not found in actions! Skipping parent. (TRIGGER?)", branch.SourceID) } if destinationFound { children[branch.SourceID] = append(children[branch.SourceID], branch.DestinationID) } else { - log.Printf("ID %s was not found in actions! Skipping child. (TRIGGER?)", branch.SourceID) + log.Printf("[INFO] ID %s was not found in actions! Skipping child. (TRIGGER?)", branch.SourceID) } } - log.Printf("Actions: %d + Special Triggers: %d", len(workflowExecution.Workflow.Actions), extra) + log.Printf("\n\n\n[INFO] CHILDREN FOUND: %#v", children) + log.Printf("[INFO] PARENTS FOUND: %#v", parents) + log.Printf("[INFO] NEXT ACTIONS: %#v\n\n", nextActions) + + log.Printf("[INFO] Actions: %d + Special Triggers: %d", len(workflowExecution.Workflow.Actions), extra) onpremApps := []string{} toExecuteOnprem := []string{} for _, action := range workflowExecution.Workflow.Actions { @@ -1730,7 +1731,7 @@ func executionInit(workflowExecution WorkflowExecution) error { pullOptions := types.ImagePullOptions{} _ = pullOptions for _, image := range onpremApps { - log.Printf("Image: %s", image) + log.Printf("[INFO] Image: %s", image) // Kind of gambling that the image exists. if strings.Contains(image, " ") { image = strings.ReplaceAll(image, " ", "-") @@ -2498,7 +2499,7 @@ func getWorkflowExecution(ctx context.Context, id string) (*WorkflowExecution, e } func validateFinished(workflowExecution WorkflowExecution) { - log.Printf("Status: %s, Actions: %d, Extra: %d, Results: %d\n", workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results)) + log.Printf("[INFO] Status: %s, Actions: %d, Extra: %d, Results: %d\n", workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results)) //if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions)+extra { if (len(environments) == 1 && requestsSent == 0 && len(workflowExecution.Results) >= 1) || (len(workflowExecution.Results) >= len(workflowExecution.Workflow.Actions) && len(workflowExecution.Workflow.Actions) > 0) { @@ -2703,7 +2704,7 @@ func main() { } else { authorization = os.Getenv("AUTHORIZATION") executionId = os.Getenv("EXECUTIONID") - log.Printf("Running normal execution with auth %s and ID %s", authorization, executionId) + log.Printf("[INFO] Running normal execution with auth %s and ID %s", authorization, executionId) } if len(authorization) == 0 { From 04e8a22a717f4d70ca0966486038e4165463df0d Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 21 Feb 2021 17:00:23 +0100 Subject: [PATCH 122/185] #247: Fixed Worker return and app SDK bug --- backend/app_sdk/app_base.py | 13 +++-- backend/app_sdk/build.sh | 2 +- backend/go-app/walkoff.go | 5 ++ frontend/src/views/Admin.jsx | 1 + frontend/src/views/AngularWorkflow.jsx | 78 +++++++++++++++++--------- functions/onprem/worker/worker.go | 26 ++++++--- 6 files changed, 83 insertions(+), 42 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 8c43637d..56a320c6 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -541,8 +541,9 @@ class AppBase: "status": "EXECUTING" } + # Simple validation of parameters in general try: - print(action["parameters"]) + tmp_parameters = action["parameters"] except KeyError: action["parameters"] = [] except TypeError: @@ -1514,11 +1515,13 @@ class AppBase: if len(json_replacement) > minlength: minlength = len(json_replacement) + + print("PRE new_replacement") # FIXME: Only do this IF they want to loop new_replacement = [] for i in range(len(json_replacement)): - if isinstance(json_replacement[i], dict) or isinstance(json_replacement[i], dict): + if isinstance(json_replacement[i], dict) or isinstance(json_replacement[i], list): tmp_replacer = json.dumps(json_replacement[i]) newvalue = tmpitem.replace(actualitem[0][0], tmp_replacer, 1) else: @@ -1727,7 +1730,7 @@ class AppBase: #print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") print("[INFO] Running normal execution\n") newres = await func(**params) - print("\n[INFO] Returned from execution:", newres) + print("\n[INFO] Returned from execution with datalength!")#, newres) if isinstance(newres, tuple): print("[INFO] Handling return as tuple") # Handles files. @@ -1753,7 +1756,7 @@ class AppBase: result = json.dumps(tmp_result) elif isinstance(newres, str): - print("[INFO] Handling return as string") + print("[INFO] Handling return as string of length %d" % len(newres)) result += newres else: try: @@ -1762,7 +1765,7 @@ class AppBase: result += "Failed autocasting. Can't handle %s type from function. Must be string" % type(newres) print("Can't handle type %s value from function" % (type(newres))) - print("[INFO] POST NEWRES RESULT: ", result) + print("[INFO] POST NEWRES RESULT!")#, result) else: #print("[INFO] APP_SDK DONE: Starting MULTI execution (length: %d) with values %s" % (minlength, multi_parameters)) # 1. Use number of executions based on the arrays being similar diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 96204598..76c9cee3 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.56 +VERSION=0.8.57 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 7fab3fc6..f49cba9c 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -6163,6 +6163,11 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) { return } + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") + requestCache.Delete(cacheKey) + resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) } diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 735814cf..62f926a4 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1122,6 +1122,7 @@ const Admin = (props) => { Edit authentication for {selectedAuthentication.app.name} ({selectedAuthentication.label}) {selectedAuthentication.fields.map((data, index) => { + console.log("DATA: ", data, selectedAuthentication) return (
    {data.key} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 2fec90dd..eb88c253 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1966,7 +1966,7 @@ const AngularWorkflow = (props) => { return (
    - What are WORKFLOW variables? + What are WORKFLOW variables? {workflow.workflow_variables === null ? null : workflow.workflow_variables.map(variable=> { return ( @@ -2033,7 +2033,7 @@ const AngularWorkflow = (props) => { }}>New workflow variable
    - What are EXECUTION variables? + What are EXECUTION variables? {workflow.execution_variables === null || workflow.execution_variables === undefined ? null : workflow.execution_variables.map(variable=> { return ( @@ -2254,9 +2254,9 @@ const AngularWorkflow = (props) => {
    {triggers.map((trigger, index) => { var imageline = trigger.large_image.length === 0 ? - + : - + const color = trigger.is_valid ? "green" : "orange" return( @@ -3935,7 +3935,7 @@ const AngularWorkflow = (props) => { - What are actions? + What are actions? {selectedAction.errors !== null && selectedAction.errors.length > 0 ?
    Errors: {selectedAction.errors.join("\n")} @@ -3969,11 +3969,13 @@ const AngularWorkflow = (props) => {
    Authenticate {selectedApp.name}: + +
    : null} @@ -4559,7 +4561,7 @@ const AngularWorkflow = (props) => { }} > - Conditions can't be used for loops [ .# ] Learn more + Conditions can't be used for loops [ .# ] Learn more Condition @@ -4568,6 +4570,7 @@ const AngularWorkflow = (props) => {
    + +
    @@ -4811,7 +4815,7 @@ const AngularWorkflow = (props) => {

    Branch: Conditions - {selectedEdgeIndex}

    - What are conditions? + What are conditions?
    @@ -5035,7 +5039,7 @@ const AngularWorkflow = (props) => {

    {selectedTrigger.app_name}: {selectedTrigger.status}

    - What are email triggers? + What are email triggers?
    @@ -5126,7 +5130,7 @@ const AngularWorkflow = (props) => {

    {selectedTrigger.app_name}

    - What are subflows? + What are subflows?
    @@ -5202,7 +5206,7 @@ const AngularWorkflow = (props) => { })} } - {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow} + {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow}
    @@ -5329,7 +5333,7 @@ const AngularWorkflow = (props) => {

    {selectedTrigger.app_name}: {selectedTrigger.status}

    - What are webhooks? + What are webhooks?
    @@ -5704,7 +5708,7 @@ const AngularWorkflow = (props) => {

    {selectedTrigger.app_name}: {selectedTrigger.status}

    - What is the user input trigger? + What is the user input trigger?
    @@ -5882,7 +5886,7 @@ const AngularWorkflow = (props) => {

    {selectedTrigger.app_name}: {selectedTrigger.status}

    - What are schedules? + What are schedules?
    @@ -6139,12 +6143,14 @@ const AngularWorkflow = (props) => {
    + +
    ) @@ -6195,12 +6201,14 @@ const AngularWorkflow = (props) => {
    + +
    ) @@ -6212,19 +6220,23 @@ const AngularWorkflow = (props) => { const boxSize = 100 const executionButton = executionRunning ? + + : - + + + return( @@ -6247,29 +6259,37 @@ const AngularWorkflow = (props) => { /> - + + + - + + + + + + + {/* */} @@ -6608,6 +6628,7 @@ const AngularWorkflow = (props) => {

    Executing Workflow

    + +
    {executionData.status !== undefined && executionData.status.length > 0 ? @@ -6627,7 +6649,7 @@ const AngularWorkflow = (props) => { {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ?
    Source:   {executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ? - Parent Workflow + Parent Workflow : executionData.execution_source } @@ -6744,7 +6766,7 @@ const AngularWorkflow = (props) => { {data.action.app_name === "shuffle-subflow" ? {validate.valid && data.action.parameters !== undefined && data.action.parameters !== null ? - See subflow execution + See subflow execution : "TBD: Load subexecution result for" } @@ -6989,7 +7011,7 @@ const AngularWorkflow = (props) => { Execution Variable - Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more here + Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more here setNewVariableName(event.target.value)} color="primary" @@ -7264,7 +7286,7 @@ const AngularWorkflow = (props) => { return (
    - What is this?
    + What is this?
    These are required fields for authenticating with {selectedApp.name}
    Name - what is this used for? diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index e09ca874..2773c459 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -1235,7 +1235,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // IF NOT VISITED && IN toExecuteOnPrem // SKIP if it's not onprem toRemove := []int{} - log.Printf("\n\nNEXTACTIONS: %#v\n\n", nextActions) + //log.Printf("\n\nNEXTACTIONS: %#v\n\n", nextActions) for index, nextAction := range nextActions { action := getAction(workflowExecution, nextAction, environment) // check visited and onprem @@ -1698,9 +1698,11 @@ func executionInit(workflowExecution WorkflowExecution) error { } } - log.Printf("\n\n\n[INFO] CHILDREN FOUND: %#v", children) - log.Printf("[INFO] PARENTS FOUND: %#v", parents) - log.Printf("[INFO] NEXT ACTIONS: %#v\n\n", nextActions) + /* + log.Printf("\n\n\n[INFO] CHILDREN FOUND: %#v", children) + log.Printf("[INFO] PARENTS FOUND: %#v", parents) + log.Printf("[INFO] NEXT ACTIONS: %#v\n\n", nextActions) + */ log.Printf("[INFO] Actions: %d + Special Triggers: %d", len(workflowExecution.Workflow.Actions), extra) onpremApps := []string{} @@ -2064,7 +2066,10 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // return //} + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp) + } func findChildNodes(workflowExecution WorkflowExecution, nodeId string) []string { @@ -2473,15 +2478,20 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl return } } else { - log.Printf("Skipping setexec with status %s", workflowExecution.Status) + log.Printf("[INFO] Skipping setexec with status %s", workflowExecution.Status) + + // Just in case. Should MAYBE validate finishing another time as well. + // This fixes issues with e.g. Action -> Trigger -> Action. + handleExecutionResult(*workflowExecution) + //validateFinished(workflowExecution) } //if newExecutions && len(nextActions) > 0 { // handleExecutionResult(*workflowExecution) //} - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) + //resp.WriteHeader(200) + //resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) } func getWorkflowExecution(ctx context.Context, id string) (*WorkflowExecution, error) { @@ -2532,7 +2542,7 @@ func validateFinished(workflowExecution WorkflowExecution) { } body, err := ioutil.ReadAll(newresp.Body) - log.Printf("BACKEND STATUS: %d", newresp.StatusCode) + log.Printf("[INFO] BACKEND STATUS: %d", newresp.StatusCode) if err != nil { log.Printf("[ERROR] Failed reading body: %s", err) } else { From 096bfbca739edf5a5be069c21a381e745024c38c Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 21 Feb 2021 17:42:02 +0100 Subject: [PATCH 123/185] #266: Fixed key:value store for options with || --- backend/app_sdk/app_base.py | 14 ++++++++++ frontend/src/views/AngularWorkflow.jsx | 36 ++++++++++++++++---------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 56a320c6..e428da9f 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1423,6 +1423,20 @@ class AppBase: for parameter in action["parameters"]: counter += 1 + # Hack for key:value in options using || + try: + if parameter["options"] != None and len(parameter["options"]) > 0: + #print(f'OPTIONS: {parameter["options"]}') + #print(f'OPTIONS VAL: {parameter}') + if "||" in parameter["value"]: + splitvalue = parameter["value"].split("||") + if len(splitvalue) > 1: + print(f'[INFO] Parsed split || options of actions["parameters"]["name"]') + action["parameters"][counter]["value"] = splitvalue[1] + + except (IndexError, KeyError, TypeError) as e: + print("Options err: {e}") + if parameter["name"] == "body": bodyindex = counter #print("PARAM: %s" % parameter) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index eb88c253..79979409 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -3355,11 +3355,19 @@ const AngularWorkflow = (props) => { }} style={{backgroundColor: surfaceColor, color: "white", height: "50px"}} > - {selectedActionParameters[count].options.map(data => ( - - {data} - - ))} + {selectedActionParameters[count].options.map((data, index) => { + const split_data = data.split("||") + var viewed_data = data + if (split_data.length > 1) { + viewed_data = split_data[0] + } + + return ( + + {viewed_data} + + ) + })} } else if (data.variant === "STATIC_VALUE") { @@ -6628,15 +6636,15 @@ const AngularWorkflow = (props) => {

    Executing Workflow

    - - + +
    From 0bd92d0fc76bf2105645ffcffc6d69fec205d79f Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 21 Feb 2021 19:01:06 +0100 Subject: [PATCH 124/185] Updated everything to 0.8.60 --- .env | 2 +- backend/app_sdk/build.sh | 2 +- backend/go-app/walkoff.go | 2 +- docker-compose.yml | 10 +++++----- functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/orborus.go | 9 +++++---- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.env b/.env index bd8738f6..3bb32c35 100644 --- a/.env +++ b/.env @@ -39,7 +39,7 @@ SHUFFLE_PASS_WORKER_PROXY=TRUE SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_NAME=frikky -SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.5" +SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.60" # Used for auto-cleanup of containers. REALLY important at scale. SHUFFLE_CONTAINER_AUTO_CLEANUP=false diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 76c9cee3..25fa59d6 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.57 +VERSION=0.8.60 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index f49cba9c..5fbdb3e6 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -3508,7 +3508,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } if !found { - log.Printf("SHOULD SET TRIGGER %s TO BE SKIPPED", trigger.ID) + //log.Printf("SHOULD SET TRIGGER %s TO BE SKIPPED", trigger.ID) curaction := Action{ AppName: trigger.AppName, diff --git a/docker-compose.yml b/docker-compose.yml index a3bc7321..67fe27a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.57 + image: ghcr.io/frikky/shuffle-frontend:0.8.60 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.56 + image: ghcr.io/frikky/shuffle-backend:0.8.60 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -45,7 +45,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.56 + image: ghcr.io/frikky/shuffle-orborus:0.8.60 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -53,8 +53,8 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - - SHUFFLE_APP_SDK_VERSION=0.8.51 - - SHUFFLE_WORKER_VERSION=0.8.56 + - SHUFFLE_APP_SDK_VERSION=0.8.60 + - SHUFFLE_WORKER_VERSION=0.8.60 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index c03ea51f..a0fcec2c 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.56 +VERSION=0.8.60 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index c98ae359..bf7f2919 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -253,11 +253,11 @@ func initializeImages() { ctx := context.Background() if appSdkVersion == "" { - appSdkVersion = "0.8.5" + appSdkVersion = "0.8.60" log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.8.57" + workerVersion = "0.8.60" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } @@ -544,9 +544,10 @@ func main() { } } + // Doesn't work because of USER INPUT if found { - log.Printf("[INFO] Skipping duplicate %s", execution.ExecutionId) - continue + //log.Printf("[INFO] Skipping duplicate %s", execution.ExecutionId) + //continue } else { //log.Printf("[INFO] Adding to be ran %s", execution.ExecutionId) executionIds = append(executionIds, execution.ExecutionId) diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 7f6f86c5..ba1b2e38 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-worker -VERSION=0.8.57 +VERSION=0.8.60 echo "Running docker build with $NAME:$VERSION" #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 2773c459..dcda8b3e 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -1624,7 +1624,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { } if shutdownCheck { - log.Println("BREAKING BECAUSE RESULTS IS SAME LENGTH AS ACTIONS. SHOULD CHECK ALL RESULTS FOR WHETHER THEY'RE DONE") + log.Println("[INFO] BREAKING BECAUSE RESULTS IS SAME LENGTH AS ACTIONS. SHOULD CHECK ALL RESULTS FOR WHETHER THEY'RE DONE") validateFinished(workflowExecution) shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) } From 177623bbc02492d4e55e6bf2b8236de7593c86cb Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 22 Feb 2021 11:03:10 +0100 Subject: [PATCH 125/185] #267: Webhook was missing URL, which made it crash. Fixed! --- backend/go-app/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 230f50ae..3031b0b5 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -17,6 +17,7 @@ import ( "log" "net" "net/http" + "net/url" "os" "os/exec" //"regexp" @@ -3558,10 +3559,13 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // bodyWrapper = string(parsedBody) //} + url := &url.URL{} newRequest := &http.Request{ + URL: url, Method: "POST", Body: ioutil.NopCloser(bytes.NewReader(b)), } + //start, startok := request.URL.Query()["start"] // OrgId: activeOrgs[0].Id, workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest) @@ -6762,6 +6766,7 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio log.Println(string(b)) newRequest := &http.Request{ + URL: &url.URL{}, Method: "POST", Body: ioutil.NopCloser(bytes.NewReader(b)), } From 9a0affd0c6a31f388977ccdbce33dba9c026191d Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 22 Feb 2021 17:29:17 +0100 Subject: [PATCH 126/185] #265: Added CONTRIBUTING and moved install guide --- .dockerignore | 15 -------- .github/CONTRIBUTING.md | 39 ++++++++++++++++++++ install-guide.md => .github/install-guide.md | 0 CONTRIBUTING.md | 17 --------- README.md | 21 +++++++++-- 5 files changed, 57 insertions(+), 35 deletions(-) delete mode 100644 .dockerignore create mode 100644 .github/CONTRIBUTING.md rename install-guide.md => .github/install-guide.md (100%) delete mode 100644 CONTRIBUTING.md diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 08fca979..00000000 --- a/.dockerignore +++ /dev/null @@ -1,15 +0,0 @@ -**/.git* -**/.gitlab-ci.yml -**/.env -**/.dockerignore -**/Dockerfile* -**/node_modules -buildSrc/libs -.gradle/ -build/ -**build/ -.idea/ -!.idea/codeStyles/codeStyleConfig.xml -.DS_Store -*.log -out/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..d3f66e5b --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing to Shuffle + +First off, thank you for contributing to [Shuffle](https://shuffler.io/)! Your talents and your contributions are greatly appreciated. With Shuffle, we aim to make cybersecurity more accessible, and keep that in mind with everything we make. + +## Opening a new issue + +If you find a bug or think of an improvement or fix, please open a [new issue](https://github.com/frikky/Shuffle/issues/new). Outline every step necessary to reproduce the bug. Include screenshots, logs and/or code examples where applicable. The more thorough you are, the better. + +## What you can work on +There are a lot of things to work on in our complex ecosystem. The most pressing issues are documentation, use-cases and content-creation, but any help is appreciated. We'll make sure you get the help you need to get started. Below is an incomplete list of items. If you see an issue, tell us or fix it! :) + +#### App Creation (Python & GUI w/OpenAPI) +As with everything else, app creation for Shuffle is made as accessibl as possible with the app editor. However, there are some instances where it can't do the job, and you'll have to write Python code. The App Editor generates OpenAPI specifications and can be widely shared, while Python apps only work for Shuffle and NSA's WALKOFF (which Shuffle is based on). You can find our [OpenAPI apps here](https://github.com/frikky/security-openapis) and our [Python apps here](https://github.com/frikky/shuffle-apps). Apps in these repositories are automatically available after installation. Shuffle apps are searchable on [https://shuffler.io](https://shuffler.io/search). + +#### Workflow creation (GUI & Conceptualizing) +Workflows are where the magic of Shuffle automation happens. Our current ones [are outlined here](https://github.com/frikky/security-openapis), and will be automatically imported into Shuffle instances in the future. They are split into Prepare and Response, but don't necessarily have to be. If you'd like to talk about workflow creation or use-cases in general, either Open a [new issue](https://github.com/frikky/shuffle-workflows/issues/new) or send us an email at [frikky@shuffler.io](mailto:frikky@shuffler.io) + +#### Documentation (Markdown) +Documentation is essential to any product, and Shuffle is no exception. Documentation in Shuffle uses markdown and is located in the [shuffle-docs](https://github.com/frikky/shuffle-docs/tree/master/docs) repository. These are then loaded into Shuffle when someone visits [https://shuffler/docs/about](https://shuffler/docs/about), then cached for later use. If you make an edit, expect it on our website in about an hour. + +#### Frontend (ReactJS) +The frontend of Shuffle is what everyone sees when they log in. Our goal here is to make it easy to get started and keep going with Shuffle - removing any blockers from the point of accessibility. If you'd like to get started, find [an issue](https://github.com/frikky/Shuffle/issues) and check the [installation guide](https://github.com/frikky/Shuffle/blob/master/install-guide.md#local-development-installation) for setting it up locally without Docker. + +#### Backend (Golang) +The backend of Shuffle is our REST API Server that runs in the background, handling all the API-calls in general, whether from users or apps. If you'd like to get started, find [an issue](https://github.com/frikky/Shuffle/issues) and check the [installation guide](https://github.com/frikky/Shuffle/blob/master/install-guide.md#local-development-installation) for setting it up locally without Docker. + +## Working on an issue + +**Shuffle** uses the [GitHub flow](https://guides.github.com/introduction/flow/index.html). All project changes are made through pull requests. +If you see an issue that you would like to work on, leave a quick comment or just get cracking. + +### License + +All contributions are made under either the **GNU Affero General Public License v3.0** or **MIT** license. See below for further details. + +* [Main project license - AGPLv3](https://github.com/frikky/Shuffle/blob/master/LICENSE) +* [Apps - MIT](https://github.com/frikky/Shuffle-apps/blob/master/LICENSE) +* [Workflows - MIT](https://github.com/frikky/Shuffle-workflows/blob/master/LICENSE) +* [Documentation - MIT](https://github.com/frikky/Shuffle-docs/blob/master/LICENSE) diff --git a/install-guide.md b/.github/install-guide.md similarity index 100% rename from install-guide.md rename to .github/install-guide.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 58990c2e..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,17 +0,0 @@ -## Contributing to Shuffle - -First off, thank you for contributing to [Shuffle](https://shuffler.io/)! Your talents and your contributions are greatly appreciated. - -### Opening a new issue - -If you find a bug or think of an improvement or fix, please open a [new issue](https://github.com/frikky/Shuffle/issues/new). Outline every step necessary to reproduce the bug. Include screenshots and code examples where applicable. The more thorough you are, the better. - -### Working on an issue - -**Shuffle** uses the [GitHub flow](https://guides.github.com/introduction/flow/index.html). All project changes are made through pull requests. - -If you see an issue that you would like to work on, be sure to leave a quick comment asking if the issue is free to be worked on. - -### License - -All contributions are made under the **GNU Affero General Public License v3.0**. [See the license page](https://github.com/frikky/Shuffle/blob/master/LICENSE) for further details. diff --git a/README.md b/README.md index a996646e..7ccf7649 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Example Shuffle webhook integration](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_webhook.png) ## Try it -* Self-hosted: Check out the [installation guide](https://github.com/frikky/shuffle/blob/master/install-guide.md) +* Self-hosted: Check out the [installation guide](https://github.com/frikky/shuffle/blob/master/.github/install-guide.md) * Cloud: Register at https://shuffler.io/register and get cooking (missing a lot of features) Please consider [sponsoring](https://github.com/sponsors/frikky) the project if you want to see more rapid development. @@ -43,7 +43,20 @@ Please consider [sponsoring](https://github.com/sponsors/frikky) the project if ![Shuffle Architecture](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_architecture.png) ## Website -https://shuffler.io +[https://shuffler.io](https://shuffler.io) + +## Contributing +We want to make the world of cybersecurity more accessible and need all the help we can get. Send an email to [frikky@shuffler](mailto:frikky@shuffler.io) and we'll make sure to give you any training you may need. + +These are the main areas to contribute in: +* Frontend (ReactJS) +* Backend (Golang) +* App Creation (Python & GUI w/OpenAPI) +* Documentation (Markdown) +* Workflow creation (GUI & Conceptualizing) +* Content Creation (Blogs, videos etc) + +Contributing guidelines for Github are outlined [here](https://github.com/frikky/Shuffle/blob/master/.github/CONTRIBUTING.md). ## Contributors ![ICPL logo](https://github.com/frikky/Shuffle/blob/launch/frontend/src/assets/img/icpl_logo.png) @@ -61,8 +74,10 @@ https://shuffler.io ## License All modular information related to Shuffle will be under MIT (anyone can use it for whatever purpose), with Shuffle itself using AGPLv3. -Apps & App SDK: MIT +Workflows: MIT +Documentation: MIT Shuffle backend: AGPLv3 +Apps, specification and App SDK: MIT ### Repository overview Below is the folder structure with a short explanation From 8d5b435fc0c11665bb0563fb77a80ac8aca74100 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 22 Feb 2021 17:57:18 +0100 Subject: [PATCH 127/185] Added and fixed img --- README.md | 31 +++++++++--------- .../src/assets/img/github_shuffle_img.png | Bin 0 -> 441059 bytes 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 frontend/src/assets/img/github_shuffle_img.png diff --git a/README.md b/README.md index 7ccf7649..895bd472 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Shuffle -[Shuffle](https://shuffler.io) is an automation platform to unify your security services (SOAR). It has thousands of premade integrations and is based on open frameworks like OpenAPI and Mitre Att&ck. The workflow editor is based on a no-code thought process to empower non-developers, and the app creator makes you able to integrate any platform in minutes. +[Shuffle](https://shuffler.io) is an automation platform focused on accessibility. We believe everyone should have access to efficient processes, and are striving to make that a possibility by making integrations for YOUR tools. Security Operations is complex, but it doesn't have to be. [![Discord](https://img.shields.io/discord/463752820026376202.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/B2CBzUm) -![Example Shuffle webhook integration](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_webhook.png) +![Example Shuffle webhook integration](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/github_shuffle_img.png) ## Try it * Self-hosted: Check out the [installation guide](https://github.com/frikky/shuffle/blob/master/.github/install-guide.md) @@ -25,22 +25,20 @@ Please consider [sponsoring](https://github.com/sponsors/frikky) the project if * [4. Real-time executions with TheHive, Cortex and MISP](https://medium.com/@Frikkylikeme/indicators-and-webhooks-with-thehive-cortex-and-misp-open-source-soar-part-4-f70cde942e59) ## Documentation -[Documentation](https://shuffler.io/docs) can be found on https://shuffler.io/docs and is written in https://github.com/frikky/shuffle-docs. +[Documentation](https://shuffler.io/docs) can be found on [https://shuffler.io/docs](https://shuffler.io/docs) and is written here: [https://github.com/frikky/shuffle-docs](https://github.com/frikky/shuffle-docs). ## Related repositories -* Apps: https://github.com/frikky/shuffle-apps -* Workflows: https://github.com/frikky/shuffle-workflows -* Security OpenAPI apps: https://github.com/frikky/security-openapis -* Documentation: https://github.com/frikky/shuffle-docs +* OpenAPI apps: [https://github.com/frikky/security-openapis](https://github.com/frikky/security-openapis) +* Documentation: [https://github.com/frikky/shuffle-docs](https://github.com/frikky/shuffle-docs) +* Workflows: [https://github.com/frikky/shuffle-workflows](https://github.com/frikky/shuffle-workflows) +* Python apps: [https://github.com/frikky/shuffle-apps](https://github.com/frikky/shuffle-apps) ## Features -* Simple workflow automation editor -* Premade apps for a number of security tools -* App creator for [OpenAPI](https://github.com/frikky/OpenAPI-security-definitions) -* Easy to learn Python library for custom apps - -## Architecture -![Shuffle Architecture](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_architecture.png) +* Simple, feature rich [workflow editor](https://shuffler.io/docs/workflows) +* App creator using [OpenAPI](https://github.com/frikky/OpenAPI-security-definitions) +* Premade apps for your security tools +* Organization and sub-organization control +* Hybrid resource sharing with shuffler.io (optional) ## Website [https://shuffler.io](https://shuffler.io) @@ -79,6 +77,9 @@ Documentation: MIT Shuffle backend: AGPLv3 Apps, specification and App SDK: MIT +## Architecture +![Shuffle Architecture](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_architecture.png) + ### Repository overview Below is the folder structure with a short explanation ```bash @@ -94,5 +95,5 @@ Below is the folder structure with a short explanation └ docker-compose.yml # Used for deployments ``` -**It's in BETA** - [Get in touch](https://shuffler.io/contact), send a mail to [frikky@shuffler.io](mailto:frikky@shuffler.io) or poke me on twitter [@frikkylikeme](https://twitter.com/frikkylikeme) +**It's in BETA (0.8.60)** - [Get in touch](https://shuffler.io/contact), send a mail to [frikky@shuffler.io](mailto:frikky@shuffler.io) or poke me on twitter [@frikkylikeme](https://twitter.com/frikkylikeme) diff --git a/frontend/src/assets/img/github_shuffle_img.png b/frontend/src/assets/img/github_shuffle_img.png new file mode 100644 index 0000000000000000000000000000000000000000..56c1a04f7c21f110cea2d9b9d2c5cc23f4ebf214 GIT binary patch literal 441059 zcmb5WcT`i`7d494t6&8|KtMoHLFv+a6cwaPmoCzz2uce*Dor|w^e(+i2^}K62}tim zI-!LgNJ#Q+?$!JI-XCv__s-B^a!k(IXRo!_nrqIr6Zl+3?#e~_izFl@SLB~PQ70iG z#gUMlOCb9ToJp*U;sF0#c>7G(k%Z*hx6{98I@u32Nl0#y$Uk|c>6)}L12xc;tNXq) z(fsjxY1%z94vv=_p{+b*JjOm zHzYsahd}JEI;AT>+7UuCx59$Y&SDeP4b0?;eZP>LGr%~FQ7fAwijPT^6!S2}V*%ZPk`ZfNRI1H?!~1T_pN-RxuDrRfH#pM$ zH8UxJv*eSG6;l?+iewOkGSOn($n`U!xR{4&Ym13#YxJK3ysbTy3zv!d=dhot^;$pN z1n(~it4IYVCMHtI6y3jGMgIHYf&^Pa5K64lj-?E`d1fqK#O$%4@2L+<;#L;Y7YBdqQz`NZctN4kCmE-|2G?P0Rw*#*<5H~qt0Bp zFM}vTwIz<$hBS5*TH61T$4ZEbI;~IExb0cg)SVpdQ0Z4~-uqsUD0D&%%8(Vs{O>BE z+6Bj@?(yAsxh{%Fus&ps{G!!aDXq-^V|NAC4&h7dm%AOXu83LuSWpn0YOK%FTBKjq zORIDBc?a|Fhx(yZFk%h=nk|I#))=V(*7Kt{r_qF*oOEwKBG$_nBcE5<8PW^pLcH2} zySln8=zcnFT>s+|ZeNq`Di$kiy~lKqj4TD9*%LTtQlX{^AOAPPrh=y@a9JVzj-FnT zHrZxDfLq_lh&i}+T=ir7Fi}y_xSfVGR6Cz8-1DEFk!_5HDEk9aRPZ%_M*NUGe6ybYCU8&UtIN z$Z=&$hD;m#@3Q*!X5rVbUn^6>a=|Fb zI^3`bi-(00$NLe(IXFLxa!ci&xDF^3BdCPA#ImuW5QaZdEGIpCOCyS1dAiiG;APhD zg*2-$Q|r>F1``pfVpC42uBoZ1tzAR(4beGv{GcNSsi-7NI1e$)N`{TsnA)`4A-z-O z{YxEl?5LBC%7y9P*F^09yXwDBbu~4m1OLp%l<1acmzS5TWt5jY_9h4^GiQR+ZVi_2 z`8?W*Q#zPS$tMe~Azpv|MOrWL|-4QIe`K9XK4xnN91J&#taL1&pHR@T`@GVb6<*R~7l-hz&GLkKBo2z6Zd_WQ+QJ}XZzOXV&z3&&k);f5Ez&!{VYcX#5BAo9 zd-jqNX9e&Ji=Rr2arTYJa~ttP2)hIV7gm?4SG~fl7PnQFb;()(AYo8Ujzcr|XZw^+ z!N~dZSFu}lM30t%Sq@oTjtyFy;HNaaLA*iK!`sM!5^|j1TUBd4C}@ zFyQKDf6Lo{m&|*SUu$chGuFf_D=TZHZCZg*z~Uarcs|72b-&#FM8bJ%S#xJ;D1=d> zN;>Ql-`Y@ZsOtKtG`6KBfSl>7bnbd#2?4#hI1SdMQUP&&CKQvOf|yF7Q&nEhAY%8Cz{m2yq_5D54j00RQ%IdCH{OpIu+alg;Zo^R z+wyLK?WUL~A4_n^Vq=;iGfZ-{ziMVM>(b?^DLDG=eCvb2Cr8K77Zt0~A&69C(jx6T z7;T3sV2ZcS*N8BK4y?hv6A9so8&MKsfyJO(EndDB?-YoBEpZS`S8G406 zuV~;_5DNAziL;=pQ2Wh`Y@0~ogVGy&4Gyz+db@nEJwjvsoG^Mzr=q1bpS9$5t#z72 zG}TJF|CP0sT$sB0=g?4PVM|BBgq#3eqnFzYmKI6p6WsaBe1#+BcAIM<$5@jVISH|b z=|0zq!E@MH5=u(S(IUelh`FUsnf>?oh0E@{i(PHOD_(fxyc3Nq1ub$+f(K@6x(=$7 zrBEz{%rkc#?ML9jCsHNdzDiV-mf{zq`bF$#y%xu7ilz>|9?T^Uns?u56+2zSyGx7^ z?CT%dFqf@aM^V$u!orn3Iwy3VqM7eZOO%Nll8g>nO7Z-&vyHW7%MI?N7Vh%eQSMb% z9^#7%b%)89aTrfTKPrL~e84{xPkLDleBPS6ik8+L$75LQPW-UY)RYs;V>nZ4I#y_> z_u+=LXT==E(6q%r^a{hl1W`#MvDl`HNqAIveGDJ}CJx}(E^hIiGv`Vya=JDAZumxX=n3y2)0I!NI zbdg+G%1pz`wyK6DySU9mX;DR*`9aY<{LJccA0aSV#JZ0@CWO4Wf$z5hVIbZ(+l<|CIZalr3$a@7L8MFW6|Q;Y0oBek7@3*T zK+dC)4<0<&IV$~SD(4gRnG$~@zN)HKY|z%=Z~1dJx%RP*P2v*g=ss$S*g5bkULbqx z(&R{u>6li*Xe>8QVJW*yBOAIkxNuS`+<%teJOkEAswo$jq##+xAlpONSNf8vsTlT_h+Y_!rSW1eIaJz%mM4i;X1Lf;2YNJ# z6jfAgYxd&|AZPZ%PexCa!;H$^{elbDr;mHMxVZj?GW=t;hh!${5%8^CX}5=7^Y06- z^i87P%()h6-;o|OkjzRFSMl0?-_}Wtsvh%p(~jq&p*baXL)mU+HV$HFITtLbkJs ziMm40uFCHW{i(z#`VGBD^jumW+dAA#tfe8kF#3=)KT}ksmHC^LdHdiEW~xNMIp^d= z;$C$3S<*w))c|kNGV2lRRB!&>eH0au;SD+nITl7IU^kd)zpv40R@UfMd-v{LdgV9f zTCWb1&IwIRIib8+fSKfcDUhC0DEURIjIw3-3DtZ}4h))nZMr5gIXQW2=F>%Lj^To_ zUxbFb-o&c1%1U_^6-t@v>S}lQ(FCD&%NepwaVJl#pu@Cf23)U*;#JRbD6H7X4NXWE zsVRM`nK&xl7j3|%>b>)r`r0*@#b!$v(A&CXM%@(_@Vm`SoRkxFFhjf`X<47G*cu8ht&5gfgbQeR) z2_qogwasgnN2#q^mYW+xT$%R14&?i8NZI!u1gp1Lo{d+3!2wHsBep;uY+`&b2Nu7s zs`<^p(9prXE0jTGdU`r3Ir$1hOG^tHiAW_cU-?8gdISM!l`9YAP+QF?satQ~dxXZW z?~E1cXY>!R10d7=b^)1nUI@nTPLSN%&s)Gg-9ZoMZ_dxtS6Iew@x4ad$#q=3LN7ST zh{eLm#k}T^_e5vX5wHN8vEsF;aG6gGBE_ka&eKyJs_D;H8FaYxlOD|2Tj`#fl!-~` z0Od{9;4(N&}h-rS|9cpsw6L;B8Xy8R8+(pbr3ei^ELC+(rD|hwaRvkPflv* zK7RyRE}9b2Cjkg+B-0-by{PBgoJ@c$(7LCnizSvGXlk`r{q*z1q zdAAVjUP-G4OeQz~@FkAXEhGX4s%0Do4e>mBPIP`m8hv4}aK{*7r~L5OL;PsnnKNfc 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

    9qGf`{`F8U@l6CWS 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

    U-w;>Nk+i># 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 XRFJd%8Cf}$--vP8c91es$92QxEox5aTJ-p) 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 literal 0 HcmV?d00001 From 1648a8921c5eb13aa8f1e5844873acba8eaefdc0 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 22 Feb 2021 18:51:23 +0100 Subject: [PATCH 128/185] Added simple curl testfiles --- backend/tests/dockerpull.sh | 2 + backend/tests/files.sh | 16 ++++++ backend/tests/hotload.sh | 1 + backend/tests/list_files.sh | 1 + backend/tests/schedules.sh | 2 +- backend/tests/users.sh | 13 +++++ frontend/src/App.jsx | 3 +- frontend/src/components/Header.js | 84 +++++++++++++------------------ frontend/src/views/Admin.jsx | 23 ++++++++- frontend/src/views/Docs.jsx | 11 ++-- 10 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 backend/tests/dockerpull.sh create mode 100755 backend/tests/files.sh create mode 100644 backend/tests/hotload.sh create mode 100644 backend/tests/list_files.sh create mode 100644 backend/tests/users.sh diff --git a/backend/tests/dockerpull.sh b/backend/tests/dockerpull.sh new file mode 100644 index 00000000..3df0d1e5 --- /dev/null +++ b/backend/tests/dockerpull.sh @@ -0,0 +1,2 @@ +#!/bin/sh +curl -XPOST http://localhost:5001/api/v1/get_docker_image -d '{"name":"frikky/shuffle:Testing_1.0.0"}' -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" diff --git a/backend/tests/files.sh b/backend/tests/files.sh new file mode 100755 index 00000000..d27a4755 --- /dev/null +++ b/backend/tests/files.sh @@ -0,0 +1,16 @@ +curl http://192.168.3.6:5001/api/v1/files/create -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -d '{"filename": "file.txt", "org_id": "b199646b-16d2-456d-9fd6-b9972e929466", "workflow_id": "global"}' + +curl http://localhost:5001/api/v1/files/create -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -d '{"filename": "file.txt", "org_id": "b199646b-16d2-456d-9fd6-b9972e929466", "workflow_id": "global"}' + +echo +curl http://localhost:5001/api/v1/files/e19cffe4-e2da-47e9-809e-904f5cb03687/upload -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -F 'shuffle_file=@files.sh' + +curl http://localhost:5001/api/v1/files/1915981b-b897-4db1-8a2e-44bc34cead3b/content -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" +curl http://localhost:5001/api/v1/files/e19cffe4-e2da-47e9-809e-904f5cb03687 -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" +curl -XDELETE http://localhost:5001/api/v1/files/e19cffe4-e2da-47e9-809e-904f5cb03687 -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" + + #r.HandleFunc("/api/v1/files/{fileId}/content", handleGetFileContent).Methods("GET", "OPTIONS") + #r.HandleFunc("/api/v1/files/create", handleCreateFile).Methods("POST", "OPTIONS") + #r.HandleFunc("/api/v1/files/{fileId}/upload", handleUploadFile).Methods("POST", "OPTIONS") + #r.HandleFunc("/api/v1/files/{fileId}", handleGetFileMeta).Methods("GET", "OPTIONS") + #r.HandleFunc("/api/v1/files/{fileId}", handleDeleteFile).Methods("DELETE", "OPTIONS") diff --git a/backend/tests/hotload.sh b/backend/tests/hotload.sh new file mode 100644 index 00000000..d55544f6 --- /dev/null +++ b/backend/tests/hotload.sh @@ -0,0 +1 @@ +curl http://localhost:5001/api/v1/apps/run_hotload -H "Authorization: Bearer e08c6f22-9a55-4557-b008-04388cc51fb0" diff --git a/backend/tests/list_files.sh b/backend/tests/list_files.sh new file mode 100644 index 00000000..4c63e246 --- /dev/null +++ b/backend/tests/list_files.sh @@ -0,0 +1 @@ +curl -XGET http://192.168.3.6:5001/api/v1/files -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" diff --git a/backend/tests/schedules.sh b/backend/tests/schedules.sh index 339f982d..dc0c15d8 100644 --- a/backend/tests/schedules.sh +++ b/backend/tests/schedules.sh @@ -1,2 +1,2 @@ # Fails cus of unmarshal -curl -XPOST http://localhost:5001/api/v1/workflows/1d9d8ce2-566e-4c3f-8a37-5d6c7d2000b5/schedule -d '{"name": "hey", "frequency": "*/1 * * * *", "execution_argument": "{\"test\": \"hey\"}"}' -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjYwZjQwNjBlNThkNzVmZDNmNzBiZWZmODhjNzk0YTc3NTMyN2FhMzEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3NodWZmbGVyLmlvL2FwaS92MS93b3JrZmxvd3MvMWQ5ZDhjZTItNTY2ZS00YzNmLThhMzctNWQ2YzdkMjAwMGI1L2V4ZWN1dGUiLCJhenAiOiIxMDMwNzY3ODIwNjE0MjQ2MTg0MjIiLCJlbWFpbCI6InNjaGVkdWxlckBzaHVmZmxlLTI0MTUxNy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NjU1Mjc1NTEsImlhdCI6MTU2NTUyMzk1MSwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAzMDc2NzgyMDYxNDI0NjE4NDIyIn0.r0EDq9fjhf_5CPTiltyfk_L3uYJp577Uy0yYPcCAl2nv50_z_oUtbWGBpQLL8gcj-NGd3g4E52Qur8k6hCMIQweLS6WAb1279vGffEoCNDfkWb3Oy-yJGP1kzwLvqFJqnHLkSWYXNWvSyWnEimW8Rryx_m1BXS5wcA8l4NIr83kS7fPZrTwjnwFSeGSThwk91DVARzapQb8r0GEgOUyHZ1aBXnV98mikzSUt-5xFKe9eMdD22YJAj0Ru-DxAxs5nOqghX4PMRysWjshjOMrlR1piPWxqAmewp8YKZDCQ5gXskpeAFBDoULT971Wsx_NCohnJsFqx1JfPS9ZYMTW2oQ" +curl -XPOST http://localhost:5001/api/v1/workflows/1d9d8ce2-566e-4c3f-8a37-5d6c7d2000b5/schedule -d '{"name": "hey", "frequency": "*/1 * * * *", "execution_argument": "{\"test\": \"hey\"}"}' -H "Authorization: Bearer WUT" diff --git a/backend/tests/users.sh b/backend/tests/users.sh new file mode 100644 index 00000000..fa78cd6b --- /dev/null +++ b/backend/tests/users.sh @@ -0,0 +1,13 @@ +curl http://localhost:5001/api/v1/users/register -H "Authorization: Bearer 36a3eb38-0070-41c6-b20a-2a3e0941d10e" -d '{"username": "username1", "password": ""}' + +echo +curl http://localhost:5001/api/v1/users -H "Authorization: Bearer 36a3eb38-0070-41c6-b20a-2a3e0941d10e" + +echo UPDATE +curl -XPUT http://localhost:5001/api/v1/users/updateuser -H "Authorization: Bearer 36a3eb38-0070-41c6-b20a-2a3e0941d10e" -d '{"user_id": "id", "role": "admin"}' + +echo +curl -XDELETE http://localhost:5001/api/v1/users/userid -H "Authorization: Bearer 36a3eb38-0070-41c6-b20a-2a3e0941d10e" + +echo +curl -XPOST http://localhost:5001/api/v1/users/generateapikey -H "Authorization: Bearer 36a3eb38-0070-41c6-b20a-2a3e0941d10e" -d '{"user_id": "390efa79-73a3-454b-8b1d-38f56eec14ad"}' diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 18456036..ceee6dbf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -33,6 +33,7 @@ import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'; import ScrollToTop from "./components/ScrollToTop"; import AlertTemplate from "./components/AlertTemplate"; import { positions, Provider } from "react-alert"; +import {isMobile} from "react-device-detect"; // Production - backend proxy forwarding in nginx var globalUrl = window.location.origin @@ -147,7 +148,7 @@ const App = (message, props) => { } /> } /> } /> - } /> + } /> { window.location.pathname = "/docs/about" }} /> } /> } /> diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index 1143a696..c7cb5e6c 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -118,6 +118,39 @@ const Header = props => { // Should be based on some path + const avatarMenu = + + { + setAnchorEl(event.currentTarget); + }}> + + + { + handleClose() + }} + > + { + event.preventDefault() + handleClose() + }}> + + Settings + + + { + event.preventDefault() + handleClose() + handleClickLogout() + }}> + Logout + + + const logoCheck = !homePage ? null : null // Handle top bar or something @@ -197,36 +230,7 @@ const Header = props => {

    - { - setAnchorEl(event.currentTarget); - }}> - - - { - handleClose() - }} - > - { - event.preventDefault() - handleClose() - }}> - - Settings - - - { - event.preventDefault() - handleClose() - handleClickLogout() - }}> - Logout - - + {avatarMenu} {userdata === undefined || userdata.admin === undefined || userdata.admin === null || !userdata.admin ? null :
    - - -
    - Logout -
    -
    - {logoCheck} - - - - - - -
    + {avatarMenu}
    -
    +
    // const loadedCheck = diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 62f926a4..83bff401 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1441,7 +1441,7 @@ const Admin = (props) => { } const organizationView = curTab === 0 && selectedOrganization.id !== undefined ? -
    +

    Organization overview

    @@ -1457,6 +1457,25 @@ const Admin = (props) => {
    :
    + + { + const elementName = "copy_element_shuffle" + const org_id = selectedOrganization.id + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + navigator.clipboard.writeText(org_id) + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + alert.info(org_id + " copied to clipboard") + } + }}> + + + {selectedOrganization.name.length > 0 ? : @@ -2034,7 +2053,7 @@ const Admin = (props) => { /* Copy the text inside the text field */ document.execCommand("copy"); - alert.info(file.id + "copied to clipboard") + alert.info(file.id + " copied to clipboard") } }}> diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 76258eaf..3f0a045b 100644 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -31,7 +31,7 @@ const hrefStyle = { } const Docs = (props) => { - const { isLoaded, globalUrl, inputColor, selectedDoc, serverside, isMobile, update} = props; + const { isLoaded, globalUrl, selectedDoc, serverside, isMobile, update} = props; const theme = useTheme(); const [data, setData] = useState(""); @@ -213,7 +213,7 @@ const Docs = (props) => { function CodeHandler(props) { return ( -
    +			
     				
     					{props.value}
     				
    @@ -286,8 +286,8 @@ const Docs = (props) => {
     
     	const mobileStyle = {
     		color: "white",
    -		marginLeft: 15,
    -		marginRight: 15,
    +		marginLeft: 25,
    +		marginRight: 25,
     		paddingBottom: 50,
     		backgroundColor: "inherit",
     		display: "flex",
    @@ -305,6 +305,7 @@ const Docs = (props) => {
     				 {
     					const path = "/docs/"+item
     					const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ")
     					return (
    -						 {window.location.pathname = path}}>{newname}
    +						 {window.location.pathname = path}}>{newname}
     					)
     				})}
     				
    
    From 0c7dbdf1bf57f3790a5a0c8154aeeca56b8a6b49 Mon Sep 17 00:00:00 2001
    From: Harduino 
    Date: Thu, 25 Feb 2021 13:54:42 +0300
    Subject: [PATCH 129/185] pass SHUFFLE_DOWNLOAD_AUTH_BRANCH to backend service
    
    ---
     docker-compose.yml | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/docker-compose.yml b/docker-compose.yml
    index 67fe27a6..bed35f39 100644
    --- a/docker-compose.yml
    +++ b/docker-compose.yml
    @@ -35,6 +35,7 @@ services:
           - SHUFFLE_FILE_LOCATION=/shuffle-files
           - ORG_ID=${ORG_ID}
           - SHUFFLE_APP_DOWNLOAD_LOCATION=${SHUFFLE_APP_DOWNLOAD_LOCATION}
    +      - SHUFFLE_DOWNLOAD_AUTH_BRANCH=${SHUFFLE_DOWNLOAD_AUTH_BRANCH}
           - SHUFFLE_DEFAULT_USERNAME=${SHUFFLE_DEFAULT_USERNAME}
           - SHUFFLE_DEFAULT_PASSWORD=${SHUFFLE_DEFAULT_PASSWORD}
           - SHUFFLE_DEFAULT_APIKEY=${SHUFFLE_DEFAULT_APIKEY}
    
    From ee8b757c3b460aea2b3444729b22a02e87dd0305 Mon Sep 17 00:00:00 2001
    From: Harduino 
    Date: Thu, 25 Feb 2021 13:58:38 +0300
    Subject: [PATCH 130/185] fix build with using golang:latest
    
    ---
     backend/Dockerfile                  |  2 +-
     functions/onprem/orborus/Dockerfile |  8 +++++++-
     functions/onprem/worker/Dockerfile  | 10 ++++++++--
     3 files changed, 16 insertions(+), 4 deletions(-)
    
    diff --git a/backend/Dockerfile b/backend/Dockerfile
    index 1856d84f..40bb50ea 100644
    --- a/backend/Dockerfile
    +++ b/backend/Dockerfile
    @@ -1,4 +1,4 @@
    -from golang as builder
    +FROM golang:1.16.0-buster as builder
     
     # Add files
     RUN mkdir /app
    diff --git a/functions/onprem/orborus/Dockerfile b/functions/onprem/orborus/Dockerfile
    index 3db4c027..3fe760c7 100644
    --- a/functions/onprem/orborus/Dockerfile
    +++ b/functions/onprem/orborus/Dockerfile
    @@ -1,4 +1,4 @@
    -from golang as builder
    +FROM golang:1.16.0-buster as builder
     
     RUN mkdir /app
     WORKDIR /app
    @@ -6,6 +6,12 @@ RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types
     
     COPY orborus.go /app/orborus.go
     RUN go mod init orborus 
    +RUN go get github.com/docker/docker/api/types && \
    +    go get github.com/docker/docker/api/types/container && \
    +    go get github.com/docker/docker/client && \
    +    go get github.com/mackerelio/go-osstat/cpu && \
    +    go get github.com/mackerelio/go-osstat/memory && \
    +    go get github.com/satori/go.uuid
     RUN go build
     RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o orborus .
     
    diff --git a/functions/onprem/worker/Dockerfile b/functions/onprem/worker/Dockerfile
    index 483ab136..a6d82d61 100644
    --- a/functions/onprem/worker/Dockerfile
    +++ b/functions/onprem/worker/Dockerfile
    @@ -1,4 +1,4 @@
    -from golang as builder
    +FROM golang:1.16.0-buster as builder
     
     WORKDIR /app
     
    @@ -9,7 +9,13 @@ RUN go get -u github.com/gorilla/mux
     RUN go get -u github.com/patrickmn/go-cache
     
     COPY worker.go /app/worker.go
    -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker .
    +RUN go env -w GO111MODULE=auto && \
    +    go get github.com/docker/docker/api/types && \
    +    go get github.com/docker/docker/api/types/container && \
    +    go get github.com/docker/docker/client && \
    +    go get github.com/gorilla/mux && \
    +    go get github.com/patrickmn/go-cache && \
    +    CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker .
     
     FROM alpine:3.12
     
    
    From 9f7f1b9830fc419bb4689e3cc8d0b1136c70a05c Mon Sep 17 00:00:00 2001
    From: Harduino 
    Date: Thu, 25 Feb 2021 14:11:41 +0300
    Subject: [PATCH 131/185] implement environment variable
     SHUFFLE_APP_FORCE_UPDATE
    
    ---
     .env                   |  1 +
     backend/go-app/main.go | 10 +++++++++-
     docker-compose.yml     |  1 +
     3 files changed, 11 insertions(+), 1 deletion(-)
    
    diff --git a/.env b/.env
    index 3bb32c35..4ffc4215 100644
    --- a/.env
    +++ b/.env
    @@ -12,6 +12,7 @@ SHUFFLE_APP_DOWNLOAD_LOCATION=https://github.com/frikky/shuffle-apps
     SHUFFLE_DOWNLOAD_AUTH_USERNAME=
     SHUFFLE_DOWNLOAD_AUTH_PASSWORD=
     SHUFFLE_DOWNLOAD_AUTH_BRANCH=
    +SHUFFLE_APP_FORCE_UPDATE=false
     
     # User config for first load. Username & PW: min length 3
     SHUFFLE_DEFAULT_USERNAME=
    diff --git a/backend/go-app/main.go b/backend/go-app/main.go
    index 3031b0b5..7cd4e069 100644
    --- a/backend/go-app/main.go
    +++ b/backend/go-app/main.go
    @@ -7448,6 +7448,14 @@ func runInit(ctx context.Context) {
     		}
     	}
     
    +	// form force-flag to download workflow apps
    +	forceUpdateEnv := os.Getenv("SHUFFLE_APP_FORCE_UPDATE")
    +	forceUpdate := false
    +	if len(forceUpdateEnv) > 0 && forceUpdateEnv == "true" {
    +		log.Printf("Forcing to rebuild apps")
    +		forceUpdate = true
    +	}
    +
     	// Getting apps to see if we should initialize a test
     	log.Printf("Getting remote workflow apps")
     	workflowapps, err := getAllWorkflowApps(ctx, 500)
    @@ -7513,7 +7521,7 @@ func runInit(ctx context.Context) {
     		//iterateAppGithubFolders(fs, dir, "", "testing")
     
     		// FIXME: Get all the apps?
    -		iterateAppGithubFolders(fs, dir, "", "", false)
    +		iterateAppGithubFolders(fs, dir, "", "", forceUpdate)
     
     		// Hotloads locally
     		location := os.Getenv("SHUFFLE_APP_HOTLOAD_FOLDER")
    diff --git a/docker-compose.yml b/docker-compose.yml
    index 67fe27a6..3afc58e8 100644
    --- a/docker-compose.yml
    +++ b/docker-compose.yml
    @@ -38,6 +38,7 @@ services:
           - SHUFFLE_DEFAULT_USERNAME=${SHUFFLE_DEFAULT_USERNAME}
           - SHUFFLE_DEFAULT_PASSWORD=${SHUFFLE_DEFAULT_PASSWORD}
           - SHUFFLE_DEFAULT_APIKEY=${SHUFFLE_DEFAULT_APIKEY}
    +      - SHUFFLE_APP_FORCE_UPDATE=${SHUFFLE_APP_FORCE_UPDATE}
           - HTTP_PROXY=${SHUFFLE_HTTP_PROXY}
           - HTTPS_PROXY=${SHUFFLE_HTTPS_PROXY}
         restart: unless-stopped
    
    From e57ec4e2d4e9d2496c615355730a94d36abaf850 Mon Sep 17 00:00:00 2001
    From: Harduino 
    Date: Thu, 25 Feb 2021 14:23:19 +0300
    Subject: [PATCH 132/185] frontend: fix Webpack issue on building Docker image
    
    ---
     frontend/Dockerfile   | 6 +-----
     frontend/package.json | 2 +-
     2 files changed, 2 insertions(+), 6 deletions(-)
    
    diff --git a/frontend/Dockerfile b/frontend/Dockerfile
    index f48b048d..cd11e347 100644
    --- a/frontend/Dockerfile
    +++ b/frontend/Dockerfile
    @@ -1,5 +1,5 @@
     # Build environment
    -FROM node as builder
    +FROM node:14 as builder
     
     RUN mkdir /usr/src/app
     
    @@ -18,10 +18,6 @@ COPY ./src /usr/src/app/src/
     COPY ./*.sh /usr/src/app/
     COPY ./*.json /usr/src/app/
     
    -# There were issues with the webpack installer from package.json
    -RUN rm -rf /usr/src/app/node_modules/webpack
    -#RUN yarn add webpack@4.42.0
    -
     RUN yarn build
     
     # Production environment
    diff --git a/frontend/package.json b/frontend/package.json
    index 0b99f34c..6805e412 100644
    --- a/frontend/package.json
    +++ b/frontend/package.json
    @@ -57,7 +57,7 @@
         "shellwords": "^0.1.1",
         "simplebar": "^4.2.3",
         "styled-components": "^4.4.0",
    -    "webpack": "^4.42.0",
    +    "webpack": "4.44.2",
         "websocket": "^1.0.30",
         "yaml": "^1.7.2",
         "yamljs": "^0.3.0",
    
    From 9fe69066223d22fdace49240b8d640ca4331460b Mon Sep 17 00:00:00 2001
    From: Harduino 
    Date: Thu, 25 Feb 2021 14:27:15 +0300
    Subject: [PATCH 133/185] improve work with Kubernetes
    
    ---
     functions/onprem/orborus/orborus.go | 2 +-
     functions/onprem/worker/worker.go   | 3 ++-
     2 files changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go
    index bf7f2919..a2e3d716 100644
    --- a/functions/onprem/orborus/orborus.go
    +++ b/functions/onprem/orborus/orborus.go
    @@ -106,7 +106,7 @@ func getThisContainerId() {
     	}
     
     	if fCol != "" {
    -		cmd := fmt.Sprintf("cat /proc/self/cgroup | grep memory | tail -1 | cut -d/ -f%s", fCol)
    +		cmd := fmt.Sprintf("cat /proc/self/cgroup | grep memory | tail -1 | cut -d/ -f%s | grep -o -E '[0-9A-z]{64}'", fCol)
     		out, err := exec.Command("bash", "-c", cmd).Output()
     		if err == nil {
     			containerId = strings.TrimSpace(string(out))
    diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go
    index dcda8b3e..259b3f06 100644
    --- a/functions/onprem/worker/worker.go
    +++ b/functions/onprem/worker/worker.go
    @@ -53,7 +53,7 @@ var containerId string
     // form container id of current running container
     func getThisContainerId() string {
     	id := ""
    -	cmd := fmt.Sprintf("cat /proc/self/cgroup | grep memory | tail -1 | cut -d/ -f3")
    +	cmd := fmt.Sprintf("cat /proc/self/cgroup | grep memory | tail -1 | cut -d/ -f3 | grep -o -E '[0-9A-z]{64}'")
     	out, err := exec.Command("bash", "-c", cmd).Output()
     	if err == nil {
     		id = strings.TrimSpace(string(out))
    @@ -861,6 +861,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
     	}
     
     	// form container id and use it as network source if it's not empty
    +	containerId = getThisContainerId()
     	if containerId != "" {
     		hostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", containerId))
     	} else {
    
    From 6777c9565906e6ea5de90677e9872950fc1b5e3d Mon Sep 17 00:00:00 2001
    From: Harduino 
    Date: Thu, 25 Feb 2021 14:31:14 +0300
    Subject: [PATCH 134/185] enhance .gitignore file
    
    ---
     .gitignore | 4 ++++
     1 file changed, 4 insertions(+)
    
    diff --git a/.gitignore b/.gitignore
    index 5bebe49f..ca9d2090 100644
    --- a/.gitignore
    +++ b/.gitignore
    @@ -18,3 +18,7 @@ functions/generated_apps
     
     backend/onprem/app_sdk/apps
     *test.py
    +
    +shuffle-database
    +*.exe
    +*debug*
    
    From 61e779d5c69471673ff58c0fb8517d97693f10d2 Mon Sep 17 00:00:00 2001
    From: Harduino 
    Date: Thu, 25 Feb 2021 14:37:47 +0300
    Subject: [PATCH 135/185] fix workflows order on import
    
    ---
     backend/go-app/walkoff.go | 31 ++++++++++++++++++++++++++-----
     1 file changed, 26 insertions(+), 5 deletions(-)
    
    diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go
    index 5fbdb3e6..fb6f86bc 100644
    --- a/backend/go-app/walkoff.go
    +++ b/backend/go-app/walkoff.go
    @@ -11,6 +11,7 @@ import (
     	"log"
     	"net/http"
     	"os"
    +	"sort"
     	"strconv"
     	"strings"
     	"time"
    @@ -4577,8 +4578,12 @@ func setExampleresult(ctx context.Context, result AppExecutionExample) error {
     
     // Hmm, so I guess this should use uuid :(
     // Consistency PLX
    -func setWorkflow(ctx context.Context, workflow Workflow, id string) error {
    +func setWorkflow(ctx context.Context, workflow Workflow, id string, optionalEditedSecondsOffset ...int) error {
     	workflow.Edited = int64(time.Now().Unix())
    +	if len(optionalEditedSecondsOffset) > 0 {
    +		workflow.Edited += int64(optionalEditedSecondsOffset[0])
    +	}
    +
     	key := datastore.NameKey("workflow", id, nil)
     
     	// New struct, to not add body, author etc
    @@ -6326,9 +6331,25 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string,
     // Onlyname is used to
     func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname, userId, orgId string) error {
     	var err error
    +	secondsOffset := 0
     
    +	// sort file names
    +	filenames := []string{}
     	for _, file := range dir {
    -		if len(onlyname) > 0 && file.Name() != onlyname {
    +		filename := file.Name()
    +		filenames = append(filenames, filename)
    +	}
    +	sort.Strings(filenames)
    +
    +	// iterate through sorted filenames
    +	for _, filename := range filenames {
    +		secondsOffset -= 10
    +		if len(onlyname) > 0 && filename != onlyname {
    +			continue
    +		}
    +
    +		file, err := fs.Stat(filename)
    +		if err != nil {
     			continue
     		}
     
    @@ -6349,7 +6370,6 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra
     			}
     		case mode.IsRegular():
     			// Check the file
    -			filename := file.Name()
     			if strings.HasSuffix(filename, ".json") {
     				path := fmt.Sprintf("%s%s", extra, file.Name())
     				fileReader, err := fs.Open(path)
    @@ -6401,9 +6421,10 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra
     						}
     					}
     				*/
    -
    +				
    +				log.Printf("Import workflow from file: %s", filename)
     				ctx := context.Background()
    -				err = setWorkflow(ctx, workflow, workflow.ID)
    +				err = setWorkflow(ctx, workflow, workflow.ID, secondsOffset)
     				if err != nil {
     					log.Printf("Failed setting (download) workflow: %s", err)
     					continue
    
    From e4eda5367760d2c524449aba8e65329696dd9ae8 Mon Sep 17 00:00:00 2001
    From: frikky 
    Date: Fri, 26 Feb 2021 07:34:21 +0100
    Subject: [PATCH 136/185] Remapped shared frontend imports for package sizing
    
    ---
     frontend/src/views/Admin.jsx           | 56 +++-------------------
     frontend/src/views/AngularWorkflow.jsx | 65 ++------------------------
     frontend/src/views/AppCreator.jsx      | 50 ++++++--------------
     frontend/src/views/Apps.jsx            | 45 ++++--------------
     frontend/src/views/Docs.jsx            | 32 ++++---------
     frontend/src/views/SettingsPage.jsx    |  6 +--
     frontend/src/views/Workflows.jsx       | 32 +------------
     7 files changed, 45 insertions(+), 241 deletions(-)
    
    diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx
    index 83bff401..9df961ac 100644
    --- a/frontend/src/views/Admin.jsx
    +++ b/frontend/src/views/Admin.jsx
    @@ -1,60 +1,18 @@
     import React, { useState, useEffect } from 'react';
     
     import { makeStyles } from '@material-ui/styles';
    +import { useTheme } from '@material-ui/core/styles';
     import {Link} from 'react-router-dom';
    -import Paper from '@material-ui/core/Paper';
    -import Card from '@material-ui/core/Card';
    -import Tooltip from '@material-ui/core/Tooltip';
    -import FormControlLabel from '@material-ui/core/FormControlLabel';
    -import Typography from '@material-ui/core/Typography';
    -import Switch from '@material-ui/core/Switch';
    -import Select from '@material-ui/core/Select';
    -import MenuItem from '@material-ui/core/MenuItem';
    -import Divider from '@material-ui/core/Divider';
    -import TextField from '@material-ui/core/TextField';
    -import Button from '@material-ui/core/Button';
    -import Tabs from '@material-ui/core/Tabs';
    -import Tab from '@material-ui/core/Tab';
    -import Grid from '@material-ui/core/Grid';
    -import List from '@material-ui/core/List';
    -import ListItem from '@material-ui/core/ListItem';
    -import ListItemText from '@material-ui/core/ListItemText';
    -import ListItemAvatar from '@material-ui/core/ListItemAvatar';
    -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
    -import IconButton from '@material-ui/core/IconButton';
    -import Avatar from '@material-ui/core/Avatar';
    -import Zoom from '@material-ui/core/Zoom';
    +
    +import {Paper, Card, Tooltip, FormControlLabel, Typography, Switch, Select, MenuItem, Divider, TextField, Button, Tabs, Tab, Grid, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction, IconButton, Avatar, Zoom,  Dialog, DialogTitle, DialogActions, DialogContent, CircularProgress } from '@material-ui/core';
    +
    +import {Edit as EditIcon, FileCopy as FileCopyIcon, Publish as PublishIcon, SelectAll as SelectAllIcon, OpenInNew as OpenInNewIcon, CloudDownload as CloudDownloadIcon, Description as DescriptionIcon, Polymer as PolymerIcon, CheckCircle as CheckCircleIcon, Close as CloseIcon, Apps as AppsIcon, Image as ImageIcon, Delete as DeleteIcon, Cached as CachedIcon, AccessibilityNew as AccessibilityNewIcon, Lock as LockIcon, Eco as EcoIcon, Schedule as ScheduleIcon, Cloud as CloudIcon, Business as BusinessIcon} from '@material-ui/icons';
    +
     import { useAlert } from "react-alert";
     import Dropzone from '../components/Dropzone';
    -
    -import { Dialog, DialogTitle, DialogActions, DialogContent } from '@material-ui/core';
    -import { useTheme } from '@material-ui/core/styles';
     import HandlePayment from './HandlePayment'
     import OrgHeader from '../components/OrgHeader'
     
    -import CircularProgress from '@material-ui/core/CircularProgress';
    -import EditIcon from '@material-ui/icons/Edit';
    -import FileCopyIcon from '@material-ui/icons/FileCopy';
    -import PublishIcon from '@material-ui/icons/Publish';
    -import SelectAllIcon from '@material-ui/icons/SelectAll';
    -import OpenInNewIcon from '@material-ui/icons/OpenInNew';
    -import CloudDownloadIcon from '@material-ui/icons/CloudDownload';
    -import DescriptionIcon from '@material-ui/icons/Description';
    -import PolymerIcon from '@material-ui/icons/Polymer';
    -import CheckCircleIcon from '@material-ui/icons/CheckCircle';
    -import CloseIcon from '@material-ui/icons/Close';
    -import AppsIcon from '@material-ui/icons/Apps';
    -import ImageIcon from '@material-ui/icons/Image';
    -import DeleteIcon from '@material-ui/icons/Delete';
    -import CachedIcon from '@material-ui/icons/Cached';
    -import AccessibilityNewIcon from '@material-ui/icons/AccessibilityNew';
    -import LockIcon from '@material-ui/icons/Lock';
    -import EcoIcon from '@material-ui/icons/Eco';
    -import ScheduleIcon from '@material-ui/icons/Schedule';
    -import CloudIcon from '@material-ui/icons/Cloud';
    -import BusinessIcon from '@material-ui/icons/Business';
    -
    -
     const useStyles = makeStyles({
     	notchedOutline: {
     		borderColor: "#f85a3e !important"
    @@ -1645,7 +1603,7 @@ const Admin = (props) => {
     				}
     
     					
    - +
    : null diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 79979409..8b10e7ff 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2,74 +2,15 @@ import React, {useState, useEffect, useLayoutEffect} from 'react'; import { useInterval } from 'react-powerhooks'; import uuid from "uuid"; - import {Link} from 'react-router-dom'; import { Prompt } from 'react-router' -import TextField from '@material-ui/core/TextField'; -import Drawer from '@material-ui/core/Drawer'; -import Button from '@material-ui/core/Button'; -import Paper from '@material-ui/core/Paper'; -import Grid from '@material-ui/core/Grid'; -import Tabs from '@material-ui/core/Tabs'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import Tab from '@material-ui/core/Tab'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import Tooltip from '@material-ui/core/Tooltip'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; -import Divider from '@material-ui/core/Divider'; -import Dialog from '@material-ui/core/Dialog'; -import Modal from '@material-ui/core/Modal'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import InputLabel from '@material-ui/core/InputLabel'; -import DialogContent from '@material-ui/core/DialogContent'; -import FormControl from '@material-ui/core/FormControl'; -import IconButton from '@material-ui/core/IconButton'; -import Menu from '@material-ui/core/Menu'; -import Input from '@material-ui/core/Input'; -import FormGroup from '@material-ui/core/FormGroup'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Typography from '@material-ui/core/Typography'; -import Checkbox from '@material-ui/core/Checkbox'; -import Breadcrumbs from '@material-ui/core/Breadcrumbs'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Switch from '@material-ui/core/Switch'; import ReactJson from 'react-json-view' import { useBeforeunload } from 'react-beforeunload'; import NestedMenuItem from "material-ui-nested-menu-item"; -import Fade from '@material-ui/core/Fade'; -import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; -import VisibilityIcon from '@material-ui/icons/Visibility'; -import DoneIcon from '@material-ui/icons/Done'; -import CloseIcon from '@material-ui/icons/Close'; -import ErrorIcon from '@material-ui/icons/Error'; -import FindReplaceIcon from '@material-ui/icons/FindReplace'; -import ArrowLeftIcon from '@material-ui/icons/ArrowLeft'; -import CachedIcon from '@material-ui/icons/Cached'; -import AddIcon from '@material-ui/icons/Add'; -import DirectionsRunIcon from '@material-ui/icons/DirectionsRun'; -import PolymerIcon from '@material-ui/icons/Polymer'; -import FormatListNumberedIcon from '@material-ui/icons/FormatListNumbered'; -import CreateIcon from '@material-ui/icons/Create'; -import PlayArrowIcon from '@material-ui/icons/PlayArrow'; -import AspectRatioIcon from '@material-ui/icons/AspectRatio'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import AppsIcon from '@material-ui/icons/Apps'; -import ScheduleIcon from '@material-ui/icons/Schedule'; -import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'; -import PauseIcon from '@material-ui/icons/Pause'; -import DeleteIcon from '@material-ui/icons/Delete'; -import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; -import SaveIcon from '@material-ui/icons/Save'; -import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; -import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; -import SettingsIcon from '@material-ui/icons/Settings'; -import LockOpenIcon from '@material-ui/icons/LockOpen'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import VpnKeyIcon from '@material-ui/icons/VpnKey'; +import {TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade} from '@material-ui/core'; + +import {ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons'; import * as cytoscape from 'cytoscape'; import * as edgehandles from 'cytoscape-edgehandles'; diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 645c77d9..6d62055e 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2,33 +2,13 @@ import React, {useState, useEffect} from 'react'; import { makeStyles } from '@material-ui/styles'; import {BrowserView, MobileView} from "react-device-detect"; +import {Paper, Typography, FormControlLabel, Button, Divider, Select, MenuItem, FormControl, Switch, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Tooltip, Breadcrumbs, CircularProgress, Chip} from '@material-ui/core'; +import {CheckCircle as CheckCircleIcon, AttachFile as AttachFileIcon, Apps as AppsIcon, ErrorOutline as ErrorOutlineIcon} from '@material-ui/icons'; + + import {Link} from 'react-router-dom'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Button from '@material-ui/core/Button'; -import Divider from '@material-ui/core/Divider'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; -import FormControl from '@material-ui/core/FormControl'; -import Switch from '@material-ui/core/Switch'; -import Dialog from '@material-ui/core/Dialog'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogActions from '@material-ui/core/DialogActions'; -import TextField from '@material-ui/core/TextField'; -import Tooltip from '@material-ui/core/Tooltip'; -import CheckCircleIcon from '@material-ui/icons/CheckCircle'; -import AttachFileIcon from '@material-ui/icons/AttachFile'; -import Breadcrumbs from '@material-ui/core/Breadcrumbs'; -import AppsIcon from '@material-ui/icons/Apps'; -import CircularProgress from '@material-ui/core/CircularProgress'; - -import Chip from '@material-ui/core/Chip'; -import ChipInput from 'material-ui-chip-input' - import YAML from 'yaml' -import ErrorOutline from '@material-ui/icons/ErrorOutline'; +import ChipInput from 'material-ui-chip-input' import { useAlert } from "react-alert"; import words from "shellwords" @@ -340,12 +320,10 @@ const AppCreator = (props) => { return response.json() }) .then((responseJson) => { - console.log("THE BODY IS HERE") setIsAppLoaded(true) if (!responseJson.success) { alert.error("Failed to verify") } else{ - console.log("HMM 2") var jsonvalid = false var tmpvalue = "" try { @@ -1303,13 +1281,13 @@ const AppCreator = (props) => { id: 'outlined-age-simple', }} > - {apikeySelection.map(data => { + {apikeySelection.map((data, index) => { if (data === undefined) { return null } return ( - + {data} )} @@ -1327,7 +1305,7 @@ const AppCreator = (props) => { const requiredColor = data.required === true ? "green" : "red" //const required = data.required === true ?
    {data.required.toString()}
    :
    {flipRequired(index)}} style={{display: "inline", color: "red", cursor: "pointer"}}>{data.required.toString()}
    return ( - +
    {flipRequired(index)}}> Required:
    {data.required.toString()}
    @@ -1367,7 +1345,7 @@ const AppCreator = (props) => { {actions.slice(0,actionAmount).map((data, index) => { var error = data.errors.length > 0 ? - + : @@ -1391,7 +1369,7 @@ const AppCreator = (props) => { const url = data.url const hasFile = data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0 return ( - + {error}
    { @@ -2057,8 +2035,8 @@ const AppCreator = (props) => { value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]} style={{backgroundColor: inputColor, color: "white", height: "50px"}} > - {categories.map(data => ( - + {categories.map((data, index) => ( + {data} ))} @@ -2332,8 +2310,8 @@ const AppCreator = (props) => { value={authenticationOption} style={{backgroundColor: inputColor, color: "white", height: "50px"}} > - {authenticationOptions.map(data => ( - + {authenticationOptions.map((data, index) => ( + {data} ))} diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 2ee8e716..050b1d27 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -2,42 +2,15 @@ import React, { useEffect } from 'react'; import { useInterval } from 'react-powerhooks'; -import AppsIcon from '@material-ui/icons/Apps'; -import Grid from '@material-ui/core/Grid'; -import Select from '@material-ui/core/Select'; -import Paper from '@material-ui/core/Paper'; -import Divider from '@material-ui/core/Divider'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import Button from '@material-ui/core/Button'; -import TextField from '@material-ui/core/TextField'; -import FormControl from '@material-ui/core/FormControl'; -import MenuItem from '@material-ui/core/MenuItem'; -import Tooltip from '@material-ui/core/Tooltip'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Switch from '@material-ui/core/Switch'; -import Input from '@material-ui/core/Input'; -import YAML from 'yaml' -import {Link} from 'react-router-dom'; -import Breadcrumbs from '@material-ui/core/Breadcrumbs'; -import ReactJson from 'react-json-view' -import Chip from '@material-ui/core/Chip'; +import {Grid, Select, Paper, Divider, ButtonBase, Button, TextField, FormControl, MenuItem, Tooltip, FormControlLabel, Switch, Input, Breadcrumbs, Chip, Dialog, DialogTitle, DialogActions, DialogContent, CircularProgress} from '@material-ui/core'; +import {Apps as AppsIcon, Cached as CachedIcon, Publish as PublishIcon, CloudDownload as CloudDownloadIcon, Edit as EditIcon, Delete as DeleteIcon} from '@material-ui/icons'; + import { useTheme } from '@material-ui/core/styles'; -import CachedIcon from '@material-ui/icons/Cached'; -import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; -import PublishIcon from '@material-ui/icons/Publish'; -import CloudDownload from '@material-ui/icons/CloudDownload'; -import EditIcon from '@material-ui/icons/Edit'; -import DeleteIcon from '@material-ui/icons/Delete'; - +import YAML from 'yaml' +import {Link} from 'react-router-dom'; +import ReactJson from 'react-json-view' import { useAlert } from "react-alert"; - -import Dialog from '@material-ui/core/Dialog'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import CircularProgress from '@material-ui/core/CircularProgress'; - import Dropzone from '../components/Dropzone'; const surfaceColor = "#27292D" @@ -420,7 +393,7 @@ const Apps = (props) => { {data.activated && data.private_id !== undefined && data.private_id.length > 0 && data.generated ? {downloadApp(data)}}> - + : null} @@ -467,7 +440,7 @@ const Apps = (props) => { color="primary" style={{marginTop: 10, marginRight: 8}} > - + : null @@ -910,6 +883,7 @@ const Apps = (props) => { }
    + {isCloud ? null : { setCursearch(event.target.value) }} /> + }
    {apps.length > 0 ? filteredApps.length > 0 ? diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 3f0a045b..58c2983f 100644 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -1,19 +1,12 @@ -import React, {useState, useEffect} from 'react'; +import React, {useState,} from 'react'; import { useTheme } from '@material-ui/core/styles'; -import Divider from '@material-ui/core/Divider'; import ReactMarkdown from 'react-markdown'; import {BrowserView, MobileView} from "react-device-detect"; -import Button from '@material-ui/core/Button'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import Typography from '@material-ui/core/Typography'; -import Paper from '@material-ui/core/Paper'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; - import {Link} from 'react-router-dom'; +import {Divider, Button, Menu, MenuItem, Typography, Paper, List} from '@material-ui/core'; + const Body = { maxWidth: '1000px', minWidth: '768px', @@ -31,13 +24,13 @@ const hrefStyle = { } const Docs = (props) => { - const { isLoaded, globalUrl, selectedDoc, serverside, isMobile, update} = props; + const { globalUrl, selectedDoc, serverside, isMobile, } = props; const theme = useTheme(); const [data, setData] = useState(""); const [firstrequest, setFirstrequest] = useState(true); const [list, setList] = useState([]); - const [listLoaded, setListLoaded] = useState(false); + const [, setListLoaded] = useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const [baseUrl, setBaseUrl] = React.useState(serverside === true ? "" : window.location.href) @@ -153,10 +146,10 @@ const Docs = (props) => { // H# if (!found) { - var elements = parent.getElementsByTagName('h3') + elements = parent.getElementsByTagName('h3') console.log(name) - var found = false - for (var key in elements) { + found = false + for (key in elements) { const element = elements[key] if (element.innerHTML === undefined) { continue @@ -221,15 +214,6 @@ const Docs = (props) => { ) } - function TextWrapper(props) { - console.log(props) - return ( - - {props.value} - - ) - } - function Heading(props) { const element = React.createElement(`h${props.level}`, {style: {marginTop: 40}}, props.children) return ( diff --git a/frontend/src/views/SettingsPage.jsx b/frontend/src/views/SettingsPage.jsx index 31dfde8d..b1f53c1c 100644 --- a/frontend/src/views/SettingsPage.jsx +++ b/frontend/src/views/SettingsPage.jsx @@ -1,11 +1,7 @@ import React, {useState, useEffect} from 'react'; -import Paper from '@material-ui/core/Paper'; -import Button from '@material-ui/core/Button'; -import Divider from '@material-ui/core/Divider'; +import {Paper, Button, Divider, TextField} from '@material-ui/core'; import {Link} from 'react-router-dom'; - -import TextField from '@material-ui/core/TextField'; import { useAlert } from "react-alert"; import { useTheme } from '@material-ui/core/styles'; diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 05bc5196..1344514d 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -1,31 +1,9 @@ import React, { useEffect} from 'react'; import { useInterval } from 'react-powerhooks'; -import Grid from '@material-ui/core/Grid'; -import Paper from '@material-ui/core/Paper'; -import Tooltip from '@material-ui/core/Tooltip'; -import Divider from '@material-ui/core/Divider'; -import Button from '@material-ui/core/Button'; -import TextField from '@material-ui/core/TextField'; -import FormControl from '@material-ui/core/FormControl'; -import IconButton from '@material-ui/core/IconButton'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Chip from '@material-ui/core/Chip'; -import Switch from '@material-ui/core/Switch'; -import Typography from '@material-ui/core/Typography'; -import Zoom from '@material-ui/core/Zoom'; +import {Grid, Paper, Tooltip, Divider, Button, TextField, FormControl, IconButton, Menu, MenuItem, FormControlLabel, Chip, Switch, Typography, Zoom, CircularProgress, Dialog, DialogTitle, DialogActions, DialogContent} from '@material-ui/core'; +import {Cached as CachedIcon, GetApp as GetAppIcon, Apps as AppsIcon, Edit as EditIcon, MoreVert as MoreVertIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Publish as PublishIcon, CloudDownload as CloudDownloadIcon} from '@material-ui/icons'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import CachedIcon from '@material-ui/icons/Cached'; -import GetAppIcon from '@material-ui/icons/GetApp'; -import AppsIcon from '@material-ui/icons/Apps'; -import EditIcon from '@material-ui/icons/Edit'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import PlayArrowIcon from '@material-ui/icons/PlayArrow'; -import AddIcon from '@material-ui/icons/Add'; -import PublishIcon from '@material-ui/icons/Publish'; //import JSONPretty from 'react-json-pretty'; //import JSONPrettyMon from 'react-json-pretty/dist/monikai' import ReactJson from 'react-json-view' @@ -35,12 +13,6 @@ import {Link} from 'react-router-dom'; import { useAlert } from "react-alert"; import ChipInput from 'material-ui-chip-input' -import Dialog from '@material-ui/core/Dialog'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; - const inputColor = "#383B40" const surfaceColor = "#27292D" From 403648a5769d86e5007fd46743902703ce6346e0 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 26 Feb 2021 08:06:45 +0100 Subject: [PATCH 137/185] #275: Added check for whether the ID exists or not instead of opening new FD's --- functions/onprem/worker/worker.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 259b3f06..7a11bb0c 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -52,6 +52,10 @@ var containerId string // form container id of current running container func getThisContainerId() string { + if len(containerId) > 0 { + return containerId + } + id := "" cmd := fmt.Sprintf("cat /proc/self/cgroup | grep memory | tail -1 | cut -d/ -f3 | grep -o -E '[0-9A-z]{64}'") out, err := exec.Command("bash", "-c", cmd).Output() From 5658849df8febb84f2bf08948a68f9e8db9a7fb3 Mon Sep 17 00:00:00 2001 From: amitk Date: Sun, 28 Feb 2021 09:07:48 +0530 Subject: [PATCH 138/185] App icon changes and new package for app icon crop --- frontend/package.json | 1 + frontend/src/views/AppCreator.jsx | 130 +++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index bb24de65..e3b88353 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "react": "^16.14.0", "react-alert": "^5.5.0", "react-alert-template-basic": "^1.0.0", + "react-avatar-editor": "^11.1.0", "react-beforeunload": "^2.2.1", "react-chartjs-2": "^2.8.0", "react-cookie": "^4.0.1", diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 5b4cb70f..27d57a24 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -32,6 +32,13 @@ import ErrorOutline from '@material-ui/icons/ErrorOutline'; import { useAlert } from "react-alert"; import words from "shellwords" +import AvatarEditor from 'react-avatar-editor'; +import AddAPhotoIcon from '@material-ui/icons/AddAPhoto'; +import AddAPhotoOutlinedIcon from '@material-ui/icons/AddAPhotoOutlined'; +import ZoomInOutlinedIcon from '@material-ui/icons/ZoomInOutlined'; +import ZoomOutOutlinedIcon from '@material-ui/icons/ZoomOutOutlined'; +import LoopIcon from '@material-ui/icons/Loop'; + const surfaceColor = "#27292D" const inputColor = "#383B40" @@ -65,6 +72,18 @@ const boxStyle = { backgroundColor: surfaceColor, } +const dividerStyle = { + marginBottom: "10px", + marginTop: "10px", + height: "1px", + width: "100%", + backgroundColor: "grey", +} + +const appIconStyle = { + marginLeft: "5px", +} + const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important" @@ -2122,8 +2141,114 @@ const AppCreator = (props) => { //
    : // - const imageData = file.length > 0 ? file : fileBase64 + const [imageUploadError, setImageUploadError] = useState(""); + const [openImageModal, setOpenImageModal] = useState(""); + const [scale, setScale] = useState(1); + const [rotate, setRotatation] = useState(0); + const [disableImageUpload, setDisableImageUpload] = useState(true); + + let imageData = fileBase64; + let croppedData = file.length > 0 ? file : fileBase64 + const imageInfo = + + const zoomIn = () => { + setScale(scale+0.1); + } + const zoomOut = () => { + setScale(scale-0.1); + } + const rotatation = () => { + setRotatation(rotate+10); + } + + const onPositionChange = () => { + setDisableImageUpload(false); + } + + const onCancelSaveAppIcon = () => { + setFile(""); + setOpenImageModal(false) + setImageUploadError("") + } + + let editor; + const setEditorRef = (imgEditor) => { editor = imgEditor; } + + const onSaveAppIcon = () => { + if(editor){ + setFile(""); + const canvas = editor.getImageScaledToCanvas(); + setFileBase64(canvas.toDataURL()); + setOpenImageModal(false) + setDisableImageUpload(true); + } + } + + const errorText = imageUploadError.length > 0 ?
    Error: {imageUploadError}
    : null + const imageUploadModalView = openImageModal ? + + +
    Upload App Icon
    + {errorText} + + setRotatation(0)} + /> + + + + + + + + + + + + + + + + + + + +
    +
    + : null; // Random names for type & autoComplete. Didn't research :^) const landingpageDataBrowser = @@ -2139,12 +2264,13 @@ const AppCreator = (props) => { {name} + {imageUploadModalView}

    General information

    Click here to learn more about app creation
    -
    {upload.click()}}> +
    {setOpenImageModal(true)}}> upload = ref} onChange={editHeaderImage} /> {imageInfo}
    From fc7a414ee67912665f26e2647bf13af6ec03ccf3 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 1 Mar 2021 01:51:12 +0100 Subject: [PATCH 139/185] Fixed some annoyances --- frontend/src/views/AppCreator.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 0b251832..401b0262 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2305,13 +2305,19 @@ const AppCreator = (props) => { {imageUploadModalView} + upload = ref} onChange={editHeaderImage} />

    General information

    Click here to learn more about app creation
    -
    {setOpenImageModal(true)}}> - upload = ref} onChange={editHeaderImage} /> +
    { + if (fileBase64.length === 0) { + upload.click() + } + + setOpenImageModal(true) + }}> {imageInfo}
    From f91307e0a2c4978c0520242e068ff95b8ab4149e Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 1 Mar 2021 11:57:48 +0100 Subject: [PATCH 140/185] Minor fixes to frontend --- README.md | 2 +- backend/go-app/walkoff.go | 7 +++++- frontend/src/views/AngularWorkflow.jsx | 8 +++---- frontend/src/views/Apps.jsx | 4 +++- functions/onprem/worker/Dockerfile | 12 ++++------ functions/onprem/worker/worker.go | 32 ++++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 895bd472..c09bd090 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ These are the main areas to contribute in: * Workflow creation (GUI & Conceptualizing) * Content Creation (Blogs, videos etc) -Contributing guidelines for Github are outlined [here](https://github.com/frikky/Shuffle/blob/master/.github/CONTRIBUTING.md). +Contributing guidelines are outlined [here](https://github.com/frikky/Shuffle/blob/master/.github/CONTRIBUTING.md). ## Contributors ![ICPL logo](https://github.com/frikky/Shuffle/blob/launch/frontend/src/assets/img/icpl_logo.png) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index fb6f86bc..9c0556db 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -195,6 +195,11 @@ type WorkflowApp struct { DocumentationUrl string `json:"documentation_url" datastore:"documentation_url"` GithubUrl string `json:"github_url" datastore:"github_url"` } + FolderMount struct { + FolderMount bool `json:"folder_mount" datastore:"folder_mount"` + SourceFolder string `json:"source_folder" datastore:"source_folder"` + DestinationFolder string `json:"destination_folder" datastore:"destination_folder"` + } Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions,noindex"` Authentication Authentication `json:"authentication" yaml:"authentication" required:false datastore:"authentication"` Tags []string `json:"tags" yaml:"tags" required:false datastore:"activated"` @@ -6421,7 +6426,7 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra } } */ - + log.Printf("Import workflow from file: %s", filename) ctx := context.Background() err = setWorkflow(ctx, workflow, workflow.ID, secondsOffset) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 8b10e7ff..4cd2a35b 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -987,7 +987,7 @@ const AngularWorkflow = (props) => { setApps(responseJson) //getAppAuthentication() - setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name))) + setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated))) setPrioritizedApps(responseJson.filter(app => internalIds.includes(app.name))) }) .catch(error => { @@ -6637,7 +6637,7 @@ const AngularWorkflow = (props) => { Actions
    - {executionData.status !== undefined && executionData.status !== "ABORTED" && executionData.status !== "FINISHED" && executionData.status !== "FAILURE" && executionData.status !== "WAITING" ? : null} + {executionData.status !== undefined && executionData.status !== "ABORTED" && executionData.status !== "FINISHED" && executionData.status !== "FAILURE" && executionData.status !== "WAITING" && !(executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING")? : null}
    {executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING" ? @@ -6712,7 +6712,7 @@ const AngularWorkflow = (props) => { }} name={"Results for "+data.action.label} /> - {data.action.app_name === "shuffle-subflow" ? + {data.action.app_name === "shuffle-subflow" && validate.result.success !== undefined && validate.result.success === true ? {validate.valid && data.action.parameters !== undefined && data.action.parameters !== null ? See subflow execution @@ -6724,7 +6724,7 @@ const AngularWorkflow = (props) => { } : -
    +
    Result  {data.result}
    diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 050b1d27..99a9b0bb 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -201,7 +201,8 @@ const Apps = (props) => { }) .then((responseJson) => { //console.log("Apps: ", responseJson) - responseJson = sortByKey(responseJson, "large_image") + //responseJson = sortByKey(responseJson, "large_image") + responseJson = sortByKey(responseJson, "generated") setApps(responseJson) setFilteredApps(responseJson) @@ -781,6 +782,7 @@ const Apps = (props) => { var tmpapps = searchableApps.filter(data => data.name.toLowerCase().includes(searchfield) || data.description.toLowerCase().includes(searchfield)) newapps.push(...tmpapps) + console.log(newapps) setFilteredApps(newapps) //if ((newapps.length === 0 || searchBackend) && !appSearchLoading) { diff --git a/functions/onprem/worker/Dockerfile b/functions/onprem/worker/Dockerfile index a6d82d61..a95b099e 100644 --- a/functions/onprem/worker/Dockerfile +++ b/functions/onprem/worker/Dockerfile @@ -2,20 +2,16 @@ FROM golang:1.16.0-buster as builder WORKDIR /app +COPY worker.go /app/worker.go +RUN go env -w GO111MODULE=auto + RUN go get -u github.com/docker/docker/api/types RUN go get -u github.com/docker/docker/api/types/container RUN go get -u github.com/docker/docker/client RUN go get -u github.com/gorilla/mux RUN go get -u github.com/patrickmn/go-cache -COPY worker.go /app/worker.go -RUN go env -w GO111MODULE=auto && \ - go get github.com/docker/docker/api/types && \ - go get github.com/docker/docker/api/types/container && \ - go get github.com/docker/docker/client && \ - go get github.com/gorilla/mux && \ - go get github.com/patrickmn/go-cache && \ - CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker . FROM alpine:3.12 diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 7a11bb0c..1672fb22 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -18,6 +18,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" dockerclient "github.com/docker/docker/client" "github.com/gorilla/mux" @@ -877,6 +878,37 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] hostConfig.AutoRemove = true } + // FIXME: Add proper foldermounts here + //log.Printf("\n\nPRE FOLDERMOUNT\n\n") + //volumeBinds := []string{"/tmp/shuffle-mount:/rules"} + //volumeBinds := []string{"/tmp/shuffle-mount:/rules"} + volumeBinds := []string{} + if len(volumeBinds) > 0 { + log.Printf("[INFO] Setting up binds for container!") + hostConfig.Binds = volumeBinds + hostConfig.Mounts = []mount.Mount{} + for _, bind := range volumeBinds { + if !strings.Contains(bind, ":") || strings.Contains(bind, "..") || strings.HasPrefix(bind, "~") { + log.Printf("[WARNING] Bind %s is invalid.", bind) + continue + } + + log.Printf("[INFO] Appending bind %s", bind) + bindSplit := strings.Split(bind, ":") + sourceFolder := bindSplit[0] + destinationFolder := bindSplit[0] + hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ + Type: mount.TypeBind, + Source: sourceFolder, + Target: destinationFolder, + }) + } + } else { + log.Printf("[WARNING] No mounted folders") + } + // hostConfig.Binds = volumeBinds + //} + config := &container.Config{ Image: image, Env: env, From 030f2e2f333f8a313152bb28f7c626a877c625f0 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 2 Mar 2021 08:58:08 +0100 Subject: [PATCH 141/185] #284: Quickfix button to see all actions --- frontend/src/views/AppCreator.jsx | 33 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 6d62055e..5ed4bcb0 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2067,8 +2067,24 @@ const AppCreator = (props) => {
    const actionView = -
    -

    Actions ({actions.length})

    +
    +
    + {actionAmount > 0 && actionAmount < actions.length ? + + : null} +
    +

    Actions ({actionAmount} / {actions.length})

    Actions are the tasks performed by an app. Read more about actions and apps here.
    @@ -2091,18 +2107,7 @@ const AppCreator = (props) => { setActionsModalOpen(true) }}>New action {/* - {actionAmount} {actions.length} - {actionAmount > 0 && actionAmount < actions.length ? null : - - } + {actionAmount} {actions.length} */}
    From 3c6e973f3d80551aac4cbce7fc9d55e4a8bc33c3 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 2 Mar 2021 08:58:32 +0100 Subject: [PATCH 142/185] BUGFIX: Subflows and workflow abort issues --- backend/go-app/main.go | 12 +- backend/go-app/walkoff.go | 82 +++++++++++++- frontend/src/defaultCytoscapeStyle.js | 2 +- frontend/src/views/AngularWorkflow.jsx | 28 +++-- functions/onprem/orborus/orborus.go | 8 +- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 148 ++++++++++++++++--------- 7 files changed, 205 insertions(+), 77 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 7cd4e069..d1d5c5aa 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3559,9 +3559,8 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // bodyWrapper = string(parsedBody) //} - url := &url.URL{} newRequest := &http.Request{ - URL: url, + URL: &url.URL{}, Method: "POST", Body: ioutil.NopCloser(bytes.NewReader(b)), } @@ -7234,9 +7233,10 @@ func runInit(ctx context.Context) { if count == 0 && err == nil && len(activeOrgs) == 1 { log.Printf("Setting up environment with org %s", activeOrgs[0].Id) item := Environment{ - Name: "Shuffle", - Type: "onprem", - OrgId: activeOrgs[0].Id, + Name: "Shuffle", + Type: "onprem", + OrgId: activeOrgs[0].Id, + Default: true, } err = setEnvironment(ctx, &item) @@ -7419,6 +7419,7 @@ func runInit(ctx context.Context) { log.Printf("Failed getting schedules during service init: %s", err) } else { log.Printf("Setting up %d schedule(s)", len(schedules)) + url := &url.URL{} for _, schedule := range schedules { if schedule.Environment == "cloud" { log.Printf("Skipping cloud schedule") @@ -7428,6 +7429,7 @@ func runInit(ctx context.Context) { //log.Printf("Schedule: %#v", schedule) job := func() { request := &http.Request{ + URL: url, Method: "POST", Body: ioutil.NopCloser(strings.NewReader(schedule.WrappedArgument)), } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 9c0556db..0588c6bd 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "os" "sort" "strconv" @@ -650,6 +651,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode log.Printf("WRAPPER BODY: \n%s", bodyWrapper) job := func() { request := &http.Request{ + URL: &url.URL{}, Method: "POST", Body: ioutil.NopCloser(strings.NewReader(bodyWrapper)), } @@ -1014,7 +1016,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Success"}`))) return } else { - //log.Printf("[WARNING] Failed to handle new execution variant: %s", err) + //log.Printf("[WARNING] Handling other execution variant: %s", err) } var actionResult ActionResult @@ -2899,6 +2901,8 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { return } + //log.Printf("\n\nINSIDE ABORT\n\n") + location := strings.Split(request.URL.String(), "/") var fileId string if location[1] == "api" { @@ -2974,6 +2978,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { workflowExecution.CompletedAt = int64(time.Now().Unix()) workflowExecution.Status = "ABORTED" + log.Printf("[INFO] Running shutdown of %s", workflowExecution.ExecutionId) lastResult := "" newResults := []ActionResult{} @@ -2996,6 +3001,79 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { workflowExecution.Result = lastResult } + addResult := true + for _, result := range workflowExecution.Results { + if result.Status != "SKIPPED" { + addResult = false + } + } + + extra := 0 + for _, trigger := range workflowExecution.Workflow.Triggers { + //log.Printf("Appname trigger (0): %s", trigger.AppName) + if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" { + extra += 1 + } + } + + parsedReason := "An error occurred during execution of this node" + reason, reasonok := request.URL.Query()["reason"] + if reasonok { + parsedReason = reason[0] + } + + if len(workflowExecution.Results) == 0 || addResult { + newaction := Action{ + ID: workflowExecution.Start, + } + + for _, action := range workflowExecution.Workflow.Actions { + if action.ID == workflowExecution.Start { + newaction = action + break + } + } + + workflowExecution.Results = append(workflowExecution.Results, ActionResult{ + Action: newaction, + ExecutionId: workflowExecution.ExecutionId, + Authorization: workflowExecution.Authorization, + Result: parsedReason, + StartedAt: workflowExecution.StartedAt, + CompletedAt: workflowExecution.StartedAt, + Status: "FAILURE", + }) + } else if len(workflowExecution.Results) >= len(workflowExecution.Workflow.Actions)+extra { + log.Printf("[INFO] DONE - Nothing to add during abort!") + } else { + //log.Printf("VALIDATING INPUT!") + node, nodeok := request.URL.Query()["node"] + if nodeok { + nodeId := node[0] + log.Printf("[INFO] Found abort node %s", nodeId) + newaction := Action{ + ID: nodeId, + } + + for _, action := range workflowExecution.Workflow.Actions { + if action.ID == nodeId { + newaction = action + break + } + } + + workflowExecution.Results = append(workflowExecution.Results, ActionResult{ + Action: newaction, + ExecutionId: workflowExecution.ExecutionId, + Authorization: workflowExecution.Authorization, + Result: parsedReason, + StartedAt: workflowExecution.StartedAt, + CompletedAt: workflowExecution.StartedAt, + Status: "FAILURE", + }) + } + } + err = setWorkflowExecution(ctx, *workflowExecution, true) if err != nil { log.Printf("Error saving workflow execution for updates when aborting %s: %s", topic, err) @@ -3517,7 +3595,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf //log.Printf("SHOULD SET TRIGGER %s TO BE SKIPPED", trigger.ID) curaction := Action{ - AppName: trigger.AppName, + AppName: "shuffle-subflow", AppVersion: trigger.AppVersion, Label: trigger.Label, Name: trigger.Name, diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index e0732bac..ab8fae1e 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -183,7 +183,7 @@ const data = [{ css: { 'background-color': "#f85a3e", 'border-color': '#f85a3e', - 'border-width': '5px', + 'border-width': '8px', 'transition-property': 'border-width', 'transition-duration': '0.25s', }, diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 4cd2a35b..0fc7178b 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -786,7 +786,7 @@ const AngularWorkflow = (props) => { curelements[i].addClass("not-executing-highlight") } - if (executionArgument.length > 0) { + if (executionArgument !== undefined && executionArgument !== null && executionArgument.length > 0) { //alert.success("Starting execution WITH an execution argument") } else { //alert.success("Starting execution") @@ -2794,8 +2794,10 @@ const AngularWorkflow = (props) => { } var exampledata = item.example === undefined ? "" : item.example + console.log("EXAMPLE: ", exampledata) // Find previous execution and their variables - if (exampledata === "" && workflowExecutions.length > 0) { + //exampledata === "" && + if (workflowExecutions.length > 0) { // Look for the ID const found = false for (var key in workflowExecutions) { @@ -3172,8 +3174,8 @@ const AngularWorkflow = (props) => { { setMenuPosition({ - top: event.pageY, - left: event.pageX, + top: event.pageY+10, + left: event.pageX+10, }) setShowDropdownNumber(count) setShowDropdown(true) @@ -3235,8 +3237,8 @@ const AngularWorkflow = (props) => { { setMenuPosition({ - top: event.pageY, - left: event.pageX, + top: event.pageY+10, + left: event.pageX+10, }) setShowDropdownNumber(count) setShowDropdown(true) @@ -3586,7 +3588,7 @@ const AngularWorkflow = (props) => { // FIXME: Should be recursive in here const icon = pathdata.type === "value" ? : pathdata.type === "list" ? : return ( - {}} + {}} onClick={() => { handleItemClick([innerdata, pathdata]) }} @@ -6663,19 +6665,25 @@ const AngularWorkflow = (props) => { if (action !== undefined && action !== null) { imgSrc = action.large_image } + + /* + if (imgSrc.length === 0) { + console.log("CHECK IF ITS A + } + */ } var actionimg = curapp === null ? null : - {data.action.app_name} + {data.action.app_name} if (triggers.length > 2) { if (data.action.app_name === "shuffle-subflow") { - actionimg = {"Shuffle + actionimg = {"Shuffle } if (data.action.app_name === "User Input") { - actionimg = {"Shuffle + actionimg = {"Shuffle } } diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index a2e3d716..30d6394a 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -101,7 +101,7 @@ func getThisContainerId() { log.Printf("[INFO] Running containerized in Docker!") default: - fCol = "3" // for backward-compatibility with production + fCol = "0" // for backward-compatibility with production log.Printf("[WARNING] RUNNING_MODE not set - defaulting to Docker (NOT Kubernetes).") } @@ -119,8 +119,10 @@ func getThisContainerId() { //docker-76c537e9a4b7c7233011f5d70e6b7f2d600b6413ac58a96519b8dca7a3f7117a.scope } } else { - containerId = "shuffle-orborus" - log.Printf("[WARNING] Failed getting container ID: %s", err) + if fCol != "0" { + containerId = "shuffle-orborus" + log.Printf("[WARNING] Failed getting container ID: %s", err) + } } } diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index ba1b2e38..907f053e 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -10,5 +10,5 @@ docker build . -t frikky/shuffle:$NAME -t frikky/shuffle:$NAME_$VERSION -t docke #docker push frikky/shuffle:$NAME_$VERSION #docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION #docker tag frikky/shuffle:0.8.51 ghcr.io/frikky/shuffle-worker:0.8.5 -docker tag frikky/shuffle:$NAME ghcr.io/frikky/shuffle-worker:0.8.52 +#docker tag frikky/shuffle:$NAME ghcr.io/frikky/shuffle-worker:0.8.52 docker push ghcr.io/frikky/$NAME:$VERSION diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 1672fb22..f3d9250a 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -11,6 +11,7 @@ import ( "log" "net" "net/http" + "net/url" "os" "os/exec" "strings" @@ -774,8 +775,22 @@ type AppExecutionExample struct { } // removes every container except itself (worker) -func shutdown(executionId, workflowId string) { +func shutdown(workflowExecution WorkflowExecution, nodeId string, reason string, handleResultSend bool) { log.Printf("[INFO] Shutdown started") + //reason := "Error in execution" + + sleepDuration := 1 + if handleResultSend { + data, err := json.Marshal(workflowExecution) + if err == nil { + sendResult(workflowExecution, data) + log.Printf("[WARNING] Sent shutdown update") + } else { + log.Printf("[WARNING] DIDNT send update") + } + + time.Sleep(time.Duration(sleepDuration) * time.Second) + } // Might not be necessary because of cleanupEnv hostconfig autoremoval if cleanupEnv == "true" && len(containerIds) > 0 { @@ -801,7 +816,20 @@ func shutdown(executionId, workflowId string) { log.Printf("[INFO] NOT cleaning up containers. IDS: %d, CLEANUP env: %s", len(containerIds), cleanupEnv) } - fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/executions/%s/abort", baseUrl, workflowId, executionId) + fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/executions/%s/abort", baseUrl, workflowExecution.Workflow.ID, workflowExecution.ExecutionId) + + path := fmt.Sprintf("?reason=%s", url.QueryEscape(reason)) + if len(nodeId) > 0 { + path += fmt.Sprintf("&node=%s", url.QueryEscape(nodeId)) + } + if len(environment) > 0 { + path += fmt.Sprintf("&env=%s", url.QueryEscape(environment)) + } + + //fmt.Println(url.QueryEscape(query)) + fullUrl += path + log.Printf("Abort URL: %s", fullUrl) + req, err := http.NewRequest( "GET", fullUrl, @@ -844,7 +872,6 @@ func shutdown(executionId, workflowId string) { log.Printf("[INFO] Failed abort request: %s", err) } - sleepDuration := 1 log.Printf("[INFO] Finished shutdown (after %d seconds).", sleepDuration) // Allows everything to finish in subprocesses time.Sleep(time.Duration(sleepDuration) * time.Second) @@ -931,7 +958,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] err = cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) if err != nil { log.Printf("[ERROR] Failed to start container in environment %s: %s", environment, err) - //shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + //shutdown(workflowExecution, workflowExecution.Workflow.ID, true) return err } @@ -1202,7 +1229,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { if exit && len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions) { log.Printf("Shutting down.") - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } // Look for the NEXT missing action @@ -1547,18 +1574,18 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) if err != nil { log.Printf("[ERROR] Failed getting %s. The couldn't be find locally, AND is missing.", image) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } buildBuf := new(strings.Builder) _, err = io.Copy(buildBuf, reader) if err != nil { log.Printf("[ERROR] Error in IO copy: %s", err) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } else { if strings.Contains(buildBuf.String(), "errorDetail") { log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), image) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } log.Printf("[INFO] Successfully downloaded %s", image) @@ -1571,7 +1598,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { if strings.Contains(err.Error(), "No such image") { //log.Printf("[WARNING] Failed deploying %s from image %s: %s", identifier, image, err) log.Printf("[ERROR] Image doesn't exist. Shutting down") - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } } } @@ -1599,18 +1626,18 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) if err != nil { log.Printf("[ERROR] Failed getting %s. The couldn't be find locally, AND is missing.", image) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } buildBuf := new(strings.Builder) _, err = io.Copy(buildBuf, reader) if err != nil { log.Printf("[ERROR] Error in IO copy: %s", err) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } else { if strings.Contains(buildBuf.String(), "errorDetail") { log.Printf("[ERROR] Docker build:\n%s\nERROR ABOVE: Trying to pull tags from: %s", buildBuf.String(), image) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } log.Printf("[INFO] Successfully downloaded %s", image) @@ -1623,7 +1650,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { if strings.Contains(err.Error(), "No such image") { //log.Printf("[WARNING] Failed deploying %s from image %s: %s", identifier, image, err) log.Printf("[ERROR] Image doesn't exist. Shutting down") - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, action.ID, err.Error(), true) } } } @@ -1663,7 +1690,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { if shutdownCheck { log.Println("[INFO] BREAKING BECAUSE RESULTS IS SAME LENGTH AS ACTIONS. SHOULD CHECK ALL RESULTS FOR WHETHER THEY'RE DONE") validateFinished(workflowExecution) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } } @@ -1781,7 +1808,7 @@ func executionInit(workflowExecution WorkflowExecution) error { //reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) //if err != nil { // log.Printf("Failed getting %s. The app is missing or some other issue", image) - // shutdown(workflowExecution.ExecutionId) + // shutdown(workflowExecution) //} ////io.Copy(os.Stdout, reader) @@ -1799,7 +1826,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W err := executionInit(workflowExecution) if err != nil { log.Printf("[INFO] Workflow setup failed: %s", workflowExecution.ExecutionId, err) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } log.Printf("Startaction: %s", startAction) @@ -1835,7 +1862,11 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W if newresp.StatusCode != 200 { log.Printf("[ERROR] Bad statuscode: %d, %s", newresp.StatusCode, string(body)) - //shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + + if strings.Contains(string(body), "Workflowexecution is already finished") { + shutdown(workflowExecution, "", "", false) + } + time.Sleep(time.Duration(sleepTime) * time.Second) continue } @@ -1849,13 +1880,13 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" { log.Printf("[INFO] Workflow %s is finished. Exiting worker.", workflowExecution.ExecutionId) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } log.Printf("[INFO] Status: %s, Results: %d, actions: %d", workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra) if workflowExecution.Status != "EXECUTING" { log.Printf("[WARNING] Exiting as worker execution has status %s!", workflowExecution.Status) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } } @@ -2545,6 +2576,34 @@ func getWorkflowExecution(ctx context.Context, id string) (*WorkflowExecution, e return &WorkflowExecution{}, errors.New("No workflowexecution defined yet") } +func sendResult(workflowExecution WorkflowExecution, data []byte) { + fullUrl := fmt.Sprintf("%s/api/v1/streams", baseUrl) + req, err := http.NewRequest( + "POST", + fullUrl, + bytes.NewBuffer([]byte(data)), + ) + + if err != nil { + log.Printf("[ERROR] Failed creating finishing request: %s", err) + shutdown(workflowExecution, "", "", false) + } + + newresp, err := topClient.Do(req) + if err != nil { + log.Printf("[ERROR] Error running finishing request: %s", err) + shutdown(workflowExecution, "", "", false) + } + + body, err := ioutil.ReadAll(newresp.Body) + log.Printf("[INFO] BACKEND STATUS: %d", newresp.StatusCode) + if err != nil { + log.Printf("[ERROR] Failed reading body: %s", err) + } else { + log.Printf("[INFO] NEWRESP (from backend): %s", string(body)) + } +} + func validateFinished(workflowExecution WorkflowExecution) { log.Printf("[INFO] Status: %s, Actions: %d, Extra: %d, Results: %d\n", workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results)) @@ -2557,34 +2616,10 @@ func validateFinished(workflowExecution WorkflowExecution) { data, err := json.Marshal(workflowExecution) if err != nil { log.Printf("[ERROR] Failed to unmarshal data for backend") - shutdown(workflowExecution.ExecutionId, "") + shutdown(workflowExecution, "", "", true) } - fullUrl := fmt.Sprintf("%s/api/v1/streams", baseUrl) - req, err := http.NewRequest( - "POST", - fullUrl, - bytes.NewBuffer([]byte(data)), - ) - - if err != nil { - log.Printf("[ERROR] Failed creating finishing request: %s", err) - shutdown(workflowExecution.ExecutionId, "") - } - - newresp, err := topClient.Do(req) - if err != nil { - log.Printf("[ERROR] Error running finishing request: %s", err) - shutdown(workflowExecution.ExecutionId, "") - } - - body, err := ioutil.ReadAll(newresp.Body) - log.Printf("[INFO] BACKEND STATUS: %d", newresp.StatusCode) - if err != nil { - log.Printf("[ERROR] Failed reading body: %s", err) - } else { - log.Printf("[INFO] NEWRESP (from backend): %s", string(body)) - } + sendResult(workflowExecution, data) } } @@ -2649,8 +2684,9 @@ func setWorkflowExecution(ctx context.Context, workflowExecution WorkflowExecuti handleExecutionResult(workflowExecution) validateFinished(workflowExecution) if dbSave { - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", false) } + return nil } @@ -2691,7 +2727,7 @@ func webserverSetup(workflowExecution WorkflowExecution) net.Listener { listener, err := getAvailablePort() if err != nil { log.Printf("Failed to created listener: %s", err) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } port := listener.Addr().(*net.TCPAddr).Port @@ -2754,14 +2790,17 @@ func main() { log.Printf("[INFO] Running normal execution with auth %s and ID %s", authorization, executionId) } + workflowExecution := WorkflowExecution{ + ExecutionId: executionId, + } if len(authorization) == 0 { log.Println("[INFO] No AUTHORIZATION key set in env") - shutdown(executionId, "") + shutdown(workflowExecution, "", "", false) } if len(executionId) == 0 { log.Println("[INFO] No EXECUTIONID key set in env") - shutdown(executionId, "") + shutdown(workflowExecution, "", "", false) } data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, executionId, authorization) @@ -2774,7 +2813,7 @@ func main() { if err != nil { log.Println("[ERROR] Failed making request builder for backend") - shutdown(executionId, "") + shutdown(workflowExecution, "", "", true) } topClient = client @@ -2802,7 +2841,6 @@ func main() { continue } - var workflowExecution WorkflowExecution err = json.Unmarshal(body, &workflowExecution) if err != nil { log.Printf("[ERROR] Failed workflowExecution unmarshal: %s", err) @@ -2837,7 +2875,7 @@ func main() { err := executionInit(workflowExecution) if err != nil { log.Printf("[INFO] Workflow setup failed: %s", workflowExecution.ExecutionId, err) - shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } go func() { @@ -2856,7 +2894,7 @@ func main() { if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" { log.Printf("[INFO] Workflow %s is finished. Exiting worker.", workflowExecution.ExecutionId) - shutdown(executionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } if workflowExecution.Status == "EXECUTING" || workflowExecution.Status == "RUNNING" { @@ -2864,11 +2902,11 @@ func main() { err = handleExecution(client, req, workflowExecution) if err != nil { log.Printf("[INFO] Workflow %s is finished: %s", workflowExecution.ExecutionId, err) - shutdown(executionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, "", "", true) } } else { log.Printf("[INFO] Workflow %s has status %s. Exiting worker.", workflowExecution.ExecutionId, workflowExecution.Status) - shutdown(executionId, workflowExecution.Workflow.ID) + shutdown(workflowExecution, workflowExecution.Workflow.ID, "", true) } time.Sleep(time.Duration(sleepTime) * time.Second) From 14a1d53bbb7a380b144ba468b0a5809596824428 Mon Sep 17 00:00:00 2001 From: amitk Date: Wed, 3 Mar 2021 09:59:06 +0530 Subject: [PATCH 143/185] Added placeholder image --- frontend/src/views/AppCreator.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 27d57a24..9d5fb272 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -38,6 +38,7 @@ import AddAPhotoOutlinedIcon from '@material-ui/icons/AddAPhotoOutlined'; import ZoomInOutlinedIcon from '@material-ui/icons/ZoomInOutlined'; import ZoomOutOutlinedIcon from '@material-ui/icons/ZoomOutOutlined'; import LoopIcon from '@material-ui/icons/Loop'; +import AddPhotoAlternateIcon from '@material-ui/icons/AddPhotoAlternate'; const surfaceColor = "#27292D" const inputColor = "#383B40" @@ -2150,7 +2151,7 @@ const AppCreator = (props) => { let imageData = fileBase64; let croppedData = file.length > 0 ? file : fileBase64 - const imageInfo = + const imageInfo = const zoomIn = () => { setScale(scale+0.1); @@ -2272,6 +2273,7 @@ const AppCreator = (props) => {
    {setOpenImageModal(true)}}> upload = ref} onChange={editHeaderImage} /> + {imageInfo}
    From de92b0f9780622bbedbfd08205e0e8ad67ef1cb0 Mon Sep 17 00:00:00 2001 From: amitk Date: Wed, 3 Mar 2021 15:06:43 +0530 Subject: [PATCH 144/185] Alternate image to be displayed if no image. --- frontend/src/views/AppCreator.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 9d5fb272..68898952 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2152,6 +2152,8 @@ const AppCreator = (props) => { let croppedData = file.length > 0 ? file : fileBase64 const imageInfo = + + const alternateImg = const zoomIn = () => { setScale(scale+0.1); @@ -2273,7 +2275,7 @@ const AppCreator = (props) => {
    {setOpenImageModal(true)}}> upload = ref} onChange={editHeaderImage} /> - + {!imageData && (alternateImg)} {imageInfo}
    From 516a6df35619412f64e6190a3aecef29b750b0ce Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 3 Mar 2021 16:16:43 +0100 Subject: [PATCH 145/185] Minor fixes to app creation process --- backend/go-app/main.go | 7 ++++++- backend/go-app/walkoff.go | 13 ++++++++----- frontend/src/views/AppCreator.jsx | 3 ++- frontend/src/views/Apps.jsx | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index d1d5c5aa..6e7bd0b4 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -6673,6 +6673,7 @@ func createFs(basepath, pathname string) (billy.Filesystem, error) { // Hotloads new apps from a folder func handleAppHotload(location string, forceUpdate bool) error { + basepath := "base" fs, err := createFs(basepath, location) if err != nil { @@ -6695,7 +6696,11 @@ func handleAppHotload(location string, forceUpdate bool) error { return err } - cacheKey := fmt.Sprintf("workflowapps-sorted") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted") requestCache.Delete(cacheKey) return nil diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 0588c6bd..f2882d9a 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -214,7 +214,7 @@ type WorkflowAppActionParameter struct { Description string `json:"description" datastore:"description,noindex" yaml:"description"` ID string `json:"id" datastore:"id" yaml:"id,omitempty"` Name string `json:"name" datastore:"name" yaml:"name"` - Example string `json:"example" datastore:"example" yaml:"example"` + Example string `json:"example" datastore:"example,noindex" yaml:"example"` Value string `json:"value" datastore:"value,noindex" yaml:"value,omitempty"` Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` Options []string `json:"options" datastore:"options" yaml:"options"` @@ -259,12 +259,12 @@ type WorkflowAppAction struct { } `json:"execution_variable" datastore:"execution_variables"` Returns struct { Description string `json:"description" datastore:"returns" yaml:"description,omitempty"` - Example string `json:"example" datastore:"example" yaml:"example"` + Example string `json:"example" datastore:"example,noindex" yaml:"example"` ID string `json:"id" datastore:"id" yaml:"id,omitempty"` Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` } `json:"returns" datastore:"returns"` AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` - Example string `json:"example" datastore:"example" yaml:"example"` + Example string `json:"example,noindex" datastore:"example" yaml:"example"` AuthNotRequired bool `json:"auth_not_required" datastore:"auth_not_required" yaml:"auth_not_required"` } @@ -327,7 +327,7 @@ type Action struct { } `json:"position,omitempty"` Priority int `json:"priority,omitempty" datastore:"priority"` AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` - Example string `json:"example,omitempty" datastore:"example"` + Example string `json:"example,omitempty" datastore:"example,noindex"` AuthNotRequired bool `json:"auth_not_required,omitempty" datastore:"auth_not_required" yaml:"auth_not_required"` Category string `json:"category" datastore:"category"` } @@ -461,7 +461,7 @@ type AuthenticationParams struct { Description string `json:"description" datastore:"description,noindex" yaml:"description"` ID string `json:"id" datastore:"id" yaml:"id"` Name string `json:"name" datastore:"name" yaml:"name"` - Example string `json:"example" datastore:"example" yaml:"example"` + Example string `json:"example" datastore:"example,noindex" yaml:"example"` Value string `json:"value,omitempty" datastore:"value,noindex" yaml:"value"` Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` Required bool `json:"required" datastore:"required" yaml:"required"` @@ -6532,9 +6532,12 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin var err error allapps := []WorkflowApp{} + + // These are slow apps to build with some funky mechanisms reservedNames := []string{ "OWA", "NLP", + "YARA", } buildLaterFirst := []buildLaterStruct{} diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 5ed4bcb0..8252f2e4 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1238,6 +1238,7 @@ const AppCreator = (props) => { }) setActions(actions) + setActionAmount(actionAmount-1) setUpdate(Math.random()) } @@ -1914,7 +1915,7 @@ const AppCreator = (props) => { {fileUploadEnabled ? { } } - runAppSearch("") + //runAppSearch("") }) .catch(error => { alert.error(error.toString()) From 5a53b34ba5d3d3dbe27abbe2ea1c8cc584496ad2 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 4 Mar 2021 04:16:09 +0100 Subject: [PATCH 146/185] #269: Fixed subflow issues --- frontend/src/views/AngularWorkflow.jsx | 87 ++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 0fc7178b..76ad8d89 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -50,6 +50,34 @@ function useWindowSize() { return size; } +function removeParam(key, sourceURL) { + if (sourceURL === undefined) { + return + } + + var rtn = sourceURL.split("?")[0], + param, + params_arr = [], + queryString = (sourceURL.indexOf("?") !== -1) ? sourceURL.split("?")[1] : ""; + + if (queryString !== "") { + params_arr = queryString.split("&"); + for (var i = params_arr.length - 1; i >= 0; i -= 1) { + param = params_arr[i].split("=")[0]; + if (param === key) { + params_arr.splice(i, 1); + } + } + rtn = rtn + "?" + params_arr.join("&"); + } + + if (rtn === "?") { + return "" + } + + return rtn; +} + const splitter = "|~|" //const referenceUrl = "https://shuffler.io/functions/webhooks/" //const referenceUrl = window.location.origin+"/api/v1/hooks/" @@ -98,6 +126,7 @@ const AngularWorkflow = (props) => { const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false) const [showSkippedActions, setShowSkippedActions] = React.useState(false) const [lastExecution, setLastExecution] = React.useState("") + const [curpath, setCurpath] = useState(typeof window === 'undefined' || window.location === undefined ? "" : window.location.pathname) // 0 = normal, 1 = just done, 2 = normal const [savingState, setSavingState] = React.useState(0) @@ -321,7 +350,7 @@ const AngularWorkflow = (props) => { }) } - const getWorkflowExecution = (id) => { + const getWorkflowExecution = (id, execution_id) => { fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", { method: 'GET', headers: { @@ -343,13 +372,19 @@ const AngularWorkflow = (props) => { setWorkflowExecutions(responseJson) const cursearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search - const tmpView = new URLSearchParams(cursearch).get("execution_id") + var tmpView = new URLSearchParams(cursearch).get("execution_id") + if (execution_id !== undefined && execution_id !== null && execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0)) { + tmpView = execution_id + } + if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { - //console.log("SHOW EXECUTION ", tmpView) const execution = responseJson.find(data => data.execution_id === tmpView) if (execution !== null && execution !== undefined) { setExecutionData(execution) setExecutionModalView(1) + + const newitem = removeParam("execution_id", cursearch) + props.history.push(curpath+newitem) } } } @@ -596,12 +631,12 @@ const AngularWorkflow = (props) => { } } - getWorkflowExecution(props.match.params.key) + getWorkflowExecution(props.match.params.key, "") } else if (responseJson.status === "FINISHED") { console.log("STOPPING BECAUSE ITS OVAH!") setExecutionRunning(false) stop() - getWorkflowExecution(props.match.params.key) + getWorkflowExecution(props.match.params.key, "") setUpdate(Math.random()) } } @@ -1528,7 +1563,7 @@ const AngularWorkflow = (props) => { getApps() getAppAuthentication() getEnvironments() - getWorkflowExecution(props.match.params.key) + getWorkflowExecution(props.match.params.key, "") getAvailableWorkflows(-1) getSettings() @@ -1536,6 +1571,9 @@ const AngularWorkflow = (props) => { const tmpView = new URLSearchParams(cursearch).get("view") if (tmpView !== undefined && tmpView !== null && tmpView === "executions") { setExecutionModalOpen(true) + + const newitem = removeParam("view", cursearch) + props.history.push(curpath+newitem) } return } @@ -5135,7 +5173,17 @@ const AngularWorkflow = (props) => { if (e.target.value.id !== workflow.id) { const startnode = e.target.value.actions.find(action => action.id === e.target.value.start) if (startnode !== undefined && startnode !== null) { + //ddsetSubworkflowStartnode(innernode) setSubworkflowStartnode(startnode) + + try { + workflow.triggers[selectedTriggerIndex].parameters[3].value = e.target.value.id + } catch { + workflow.triggers[selectedTriggerIndex].parameters[3] = { + "name": "startnode", + "value": e.target.value.id, + } + } } console.log("STARTNODE: ", startnode) } @@ -6236,7 +6284,7 @@ const AngularWorkflow = (props) => { @@ -6482,7 +6530,7 @@ const AngularWorkflow = (props) => { style={{borderRadius: "0px"}} variant="outlined" onClick={() => { - getWorkflowExecution(props.match.params.key) + getWorkflowExecution(props.match.params.key, "") }} color="primary"> Refresh executions @@ -6562,7 +6610,7 @@ const AngularWorkflow = (props) => { { setExecutionRunning(false) stop() - getWorkflowExecution(props.match.params.key) + getWorkflowExecution(props.match.params.key, "") setExecutionModalView(0) setLastExecution(executionData.execution_id) }}> @@ -6600,7 +6648,14 @@ const AngularWorkflow = (props) => { {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ?
    Source:   {executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ? - Parent Workflow + executionData.execution_source === props.match.params.key ? + { + getWorkflowExecution(props.match.params.key, executionData.execution_parent) + }}> + Parent Execution + + : + Parent Workflow : executionData.execution_source } @@ -6659,7 +6714,7 @@ const AngularWorkflow = (props) => { const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? "green" : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : "orange" var imgSrc = curapp === undefined ? "" : curapp.large_image - if (imgSrc.length === 0) { + if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) { // Look for the node in the workflow const action = workflow.actions.find(action => action.id === data.action.id) if (action !== undefined && action !== null) { @@ -6723,7 +6778,15 @@ const AngularWorkflow = (props) => { {data.action.app_name === "shuffle-subflow" && validate.result.success !== undefined && validate.result.success === true ? {validate.valid && data.action.parameters !== undefined && data.action.parameters !== null ? - See subflow execution + data.action.parameters[0].value === props.match.params.key ? + { + getWorkflowExecution(props.match.params.key, validate.result.execution_id) + }}> + See sub-execution + + : + { + }}>See subflow execution : "TBD: Load subexecution result for" } From 62e7fa09d84ca73b1b22751ef903ec0b70f4141c Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 4 Mar 2021 08:17:43 +0100 Subject: [PATCH 147/185] BUG: Fixed bug in app sdk for OpenAPI lists --- backend/app_sdk/app_base.py | 194 ++++++++++++++++++++----- backend/go-app/codegen.go | 147 ++++++++++--------- backend/go-app/docker.go | 1 - backend/go-app/main.go | 12 +- backend/go-app/walkoff.go | 4 - frontend/src/views/AngularWorkflow.jsx | 29 ++-- 6 files changed, 256 insertions(+), 131 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index e428da9f..748ba455 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -147,6 +147,7 @@ class AppBase: #self.action = action loopnames = [] + print(f"Baseparams to check!!: {baseparams}") for key, value in baseparams.items(): check_value = "" for param in self.action["parameters"]: @@ -160,6 +161,7 @@ class AppBase: self.result_wrapper_count = octothorpe_count print("[INFO] NEW OCTOTHORPE WRAPPER: %d" % octothorpe_count) + # This whole thing is hard. # item = [{"data": "1.2.3.4", "dataType": "ip"}] # $item = DONT loop items. @@ -178,12 +180,40 @@ class AppBase: # FIXME: Check the above, and fix so that nested looped items can be # Skipped if wanted - print("\nCHECK: %s" % check_value) + #print("\nCHECK: %s" % check_value) + #try: + # values = parameter["value_replace"] + # if values != None: + # print(values) + # for val in values: + # print(val) + #except: + # pass + should_merge = False if "#" in check_value: should_merge = True + # Specific for OpenAPI body replacement + print("\n\n\nDOING STUFF BELOW HERE") + if not should_merge: + for parameter in self.action["parameters"]: + if parameter["name"] == key: + print("CHECKING BODY FOR VALUE REPLACE DATA!") + try: + values = parameter["value_replace"] + if values != None: + print(values) + for val in values: + if "#" in val["value"]: + should_merge = True + break + except: + pass + + print(f"MERGE: {should_merge}") if isinstance(value, list): + print("Item {value} is a list.") if len(value) <= 1: if len(value) == 1: baseparams[key] = value[0] @@ -206,7 +236,7 @@ class AppBase: all_list_keys.append(key) all_lists.append(baseparams[key]) else: - print("%s is not a list: " % value) + print(f"{value} is not a list") print("Listlengths: %s" % listlengths) if len(listlengths) == 0: @@ -271,20 +301,25 @@ class AppBase: # Runs recursed versions with inner loops and such async def run_recursed_items(self, func, baseparams, loop_wrapper): + print(f"RECURSED ITEMS: {baseparams}") has_loop = False newparams = {} for key, value in baseparams.items(): if isinstance(value, list) and len(value) > 0: - print("In list check") + print(f"In list check for {key}") + try: - value[0] = json.loads(value[0]) + # Added skip for body (OpenAPI) which uses data= in requests + # Can be screwed up if they name theirs body too + if key != "body": + value[0] = json.loads(value[0]) except json.decoder.JSONDecodeError as e: print("JSON casting error: %s" % e) except TypeError as e: print("TypeError: %s" % e) - print("POST list check") + print("POST initial list check") if isinstance(value, list) and len(value) == 1 and isinstance(value[0], list): try: @@ -294,12 +329,11 @@ class AppBase: except KeyError: loop_wrapper[key] = 1 - print("Key %s is a list: %s" % (key, value)) + print(f"Key {key} is a list: {value}") newparams[key] = value[0] has_loop = True else: - print("Key %s is NOT a list within a list" % (key)) - + print(f"Key {key} is NOT a list within a list. Value: {value}") newparams[key] = value results = [] @@ -315,6 +349,7 @@ class AppBase: print("[INFO] Multiplier length: %d" % len(param_multiplier)) for subparams in param_multiplier: + print(f"SUBPARAMS IN MULTI: {subparams}") try: tmp = await func(**subparams) except: @@ -1361,6 +1396,7 @@ class AppBase: return True, "" + # THE START IS ACTUALLY RIGHT HERE :O # Checks whether conditions are met, otherwise set branchcheck, tmpresult = check_branch_conditions(action, fullexecution) if not branchcheck: @@ -1437,6 +1473,10 @@ class AppBase: except (IndexError, KeyError, TypeError) as e: print("Options err: {e}") + # This part is purely for OpenAPI accessibility. + # It replaces the data back into the main item + # Earlier, we handled each of the items and did later string replacement, + # but this has changed to do lists within items and such if parameter["name"] == "body": bodyindex = counter #print("PARAM: %s" % parameter) @@ -1445,16 +1485,27 @@ class AppBase: if values != None: added = 0 for val in values: - newparams.append({ - "name": val["key"], - "value": val["value"], - "variant": "STATIC_VALUE", - "id": "body_replacement", - }) + print(f"VAL: {val}") + #parameter["value"].replace(val["key"], val["value"], -1) + print(f'PARAM1: {action["parameters"][counter]["value"]}') + action["parameters"][counter]["value"] = action["parameters"][counter]["value"].replace(val["key"], val["value"], 1) + #action["parameters"][counter]["value"].replace(r"${url}", r"$Find_URLs.valid.#.data", 1) + print(f'PARAM2: {action["parameters"][counter]["value"]}') + #newparams.append({ + # "name": val["key"], + # "value": val["value"], + # "variant": "STATIC_VALUE", + # "id": "body_replacement", + # "schema": { + # "type": "string", + # }, + #}) - print("Added param %s for body" % val["key"]) + print(f'[INFO] Added param {val["key"]} for body with value {val["value"]} (using OpenAPI)') added += 1 + #action["parameters"]["body"] + print("ADDED %d parameters for body" % added) except KeyError as e: print("KeyError body OpenAPI: %s" % e) @@ -1462,6 +1513,7 @@ class AppBase: break + print(action["parameters"]) for parameter in newparams: action["parameters"].append(parameter) @@ -1478,6 +1530,7 @@ class AppBase: multi_parameters = json.loads(json.dumps(params)) multiexecution = False multi_execution_lists = [] + remove_params = [] for parameter in action["parameters"]: check, value, is_loop = parse_params(action, fullexecution, parameter) if check: @@ -1506,6 +1559,7 @@ class AppBase: print("Before first part in multiexec!") handled = False if len(actualitem[0]) > 2 and actualitem[0][1] == "SHUFFLE_NO_SPLITTER": + print("(1) Pre replacement: %s" % actualitem[0][2]) tmpitem = value @@ -1555,10 +1609,10 @@ class AppBase: tmpitem = tmpitem.replace(actualitem[0][0], replacement, 1) # This code handles files. - print("(1) ------------ PARAM: %s" % parameter["schema"]["type"]) resultarray = [] isfile = False try: + print("(1) ------------ PARAM: %s" % parameter["schema"]["type"]) if parameter["schema"]["type"] == "file" and len(value) > 0: print("(1) SHOULD HANDLE FILE IN MULTI. Get based on value %s" % tmpitem) # This is silly :) @@ -1572,8 +1626,10 @@ class AppBase: print("(1) FILE VALUE FOR VAL %s: %s" % (tmp_file_split, file_value)) isfile = True + except NameError as e: + print("(1) SCHEMA NAMEERROR IN FILE HANDLING: %s" % e) except KeyError as e: - print("(1) SCHEMA ERROR IN FILE HANDLING: %s" % e) + print("(1) SCHEMA KEYERROR IN FILE HANDLING: %s" % e) except json.decoder.JSONDecodeError as e: print("(1) JSON ERROR IN FILE HANDLING: %s" % e) @@ -1589,7 +1645,7 @@ class AppBase: multi_execution_lists.append(new_replacement) #print("MULTI finished: %s" % json_replacement) else: - print("(2) Pre replacement. ") #% actualitem) + print("(2) Pre replacement (loop with variables). ") #% actualitem) # This is here to handle for loops within variables.. kindof # 1. Find the length of the longest array # 2. Build an array with the base values based on parameter["value"] @@ -1664,22 +1720,56 @@ class AppBase: multi_execution_lists.append(resultarray) multi_parameters[parameter["name"]] = resultarray + + #if parameter["id"] == "body_replacement": + # print("Should run body MULTI replacement in index %d with %s" % (bodyindex, parameter)) + # try: + # print("PREBODY: %s" % params["body"]) + + # parsedarray = str(resultarray) + # try: + # parsedarray = json.dumps(resultarray) + # except: + # pass + + # if f'\"{parameter["name"]}\"' in params["body"]: + # params["body"] = params["body"].replace(f'\"{parameter["name"]}\"' , parsedarray, -1) + # multi_parameters["body"] = multi_parameters["body"].replace(f'\"{parameter["name"]}\"' , parsedarray, -1) + # else: + # params["body"] = params["body"].replace(parameter["name"], parsedarray, -1) + # multi_parameters["body"] = multi_parameters["body"].replace(parameter["name"], parsedarray, -1) + + # #print("POSTBODY: %s" % params["body"]) + # #if isinstance(multi_parameters, list): + # # print("MULTIPARAM AS LIST (NOT REPLACING!!)!") + # # for multiparam in multi_parameters: + # # print(f"MULTIPARAM: {multiparam}") + # # #multi_parameters["body"] = multi_parameters["body"].replace(parameter["name"], str(parameter["value"]), -1) + # #else: + + # except KeyError as e: + # print("KEYERROR: %s" % e) + + # remove_params.append(parameter["name"]) + # #bodyindex = counter + # continue + else: # Parses things like int(value) print("Normal parsing (not looping)")#with data %s" % value) value = parse_wrapper_start(value) - if parameter["id"] == "body_replacement": - print("Should run body replacement in index %d with %s" % (bodyindex, parameter)) - try: - print("PREBODY: %s" % params["body"]) - params["body"] = params["body"].replace(parameter["name"], parameter["value"], -1) - print("POSTBODY: %s" % params["body"]) - except KeyError as e: - print("KEYERROR: %s" % e) + #if parameter["id"] == "body_replacement": + # print("Should run body replacement in index %d with %s" % (bodyindex, parameter)) + # try: + # print("PREBODY: %s" % params["body"]) + # params["body"] = params["body"].replace(parameter["name"], parameter["value"], -1) + # print("POSTBODY: %s" % params["body"]) + # except KeyError as e: + # print("KEYERROR: %s" % e) - #bodyindex = counter - continue + # #bodyindex = counter + # continue #for parameter in action["parameters"]: #if parameter["name"] == "body": @@ -1702,6 +1792,8 @@ class AppBase: except KeyError as e: print("SCHEMA ERROR IN FILE HANDLING: %s" % e) + + #remove_params.append(parameter["name"]) # Fix lists here # FIXME: This doesn't really do anything anymore print("CHECKING multi execution list!") @@ -1723,7 +1815,7 @@ class AppBase: #print("New list length: %d" % len(filteredlist)) if len(filteredlist) > 1: - print("Calculating new multi-loop length with %d lists" % len(filteredlist)) + print(f"Calculating new multi-loop length with {len(filteredlist)} lists") tmplength = 1 for innerlist in filteredlist: tmplength = len(innerlist)*tmplength @@ -1732,6 +1824,25 @@ class AppBase: minlength = tmplength print("New multi execution length: %d\n" % tmplength) + + # Cleaning up extra list params + for subparam in remove_params: + #print(f"DELETING {subparam}") + try: + del params[subparam] + except: + pass + #print(f"Error with subparam deletion of {subparam} in {params}") + try: + del multi_parameters[subparam] + except: + #print(f"Error with subparam deletion of {subparam} in {multi_parameters} (2)") + pass + + print() + print(f"Param: {params}") + print(f"Multiparams: {multi_parameters}") + print() if not multiexecution: #newparams.append({ @@ -1744,7 +1855,7 @@ class AppBase: #print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") print("[INFO] Running normal execution\n") newres = await func(**params) - print("\n[INFO] Returned from execution with datalength!")#, newres) + print("\n[INFO] Returned from execution!")#, newres) if isinstance(newres, tuple): print("[INFO] Handling return as tuple") # Handles files. @@ -1772,6 +1883,17 @@ class AppBase: elif isinstance(newres, str): print("[INFO] Handling return as string of length %d" % len(newres)) result += newres + elif isinstance(newres, dict) or isinstance(newres, list): + try: + result += json.dumps(newres, indent=4) + except json.JSONDecodeError as e: + print("Failed decoding result: %s" % e) + + try: + result += str(newres) + except ValueError: + result += "Failed autocasting. Can't handle %s type from function. Must be string" % type(newres) + print("Can't handle type %s value from function" % (type(newres))) else: try: result += str(newres) @@ -1898,10 +2020,16 @@ class AppBase: # Dump the result as a string of a list #print("RESULTS: %s" % results) - if isinstance(results, list): + if isinstance(results, list) or isinstance(results, dict): print("JSON OBJECT? ", json_object) + + # This part is weird lol if json_object: - result = json.dumps(results) + try: + result = json.dumps(results) + except json.JSONDecodeError as e: + print(f"Failed to decode: {e}") + result = results else: result = "[" for item in results: @@ -1926,7 +2054,7 @@ class AppBase: else: print("Normal result - no list?") result = results - + print("RESULT: %s" % result) action_result["status"] = "SUCCESS" action_result["result"] = str(result) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 74776e88..eecc0ee7 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -1010,16 +1010,6 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Schema: SchemaDefinition{ - Type: "string", - }, - }) headersFound := []string{} if len(path.Connect.Parameters) > 0 { @@ -1106,6 +1096,17 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ } } + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { @@ -1145,16 +1146,6 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor // FIXME - remove this when authentication is properly introduced parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify the SSL certificate request", - Multiline: false, - Required: false, - Example: "False - default=True", - Schema: SchemaDefinition{ - Type: "string", - }, - }) headersFound := []string{} if len(path.Get.Parameters) > 0 { @@ -1241,6 +1232,17 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify the SSL certificate request", + Multiline: false, + Required: false, + Example: "False - default=True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { @@ -1280,16 +1282,6 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Schema: SchemaDefinition{ - Type: "string", - }, - }) headersFound := []string{} if len(path.Head.Parameters) > 0 { @@ -1374,6 +1366,17 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { @@ -1413,16 +1416,6 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Schema: SchemaDefinition{ - Type: "string", - }, - }) headersFound := []string{} if len(path.Delete.Parameters) > 0 { @@ -1508,6 +1501,17 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] } } + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { @@ -1546,16 +1550,6 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Schema: SchemaDefinition{ - Type: "string", - }, - }) fileField := "" if path.Post.RequestBody != nil { @@ -1674,6 +1668,17 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { @@ -1718,16 +1723,6 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Schema: SchemaDefinition{ - Type: "string", - }, - }) headersFound := []string{} if len(path.Patch.Parameters) > 0 { @@ -1812,6 +1807,17 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W } } + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { @@ -1851,16 +1857,6 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Schema: SchemaDefinition{ - Type: "string", - }, - }) headersFound := []string{} if len(path.Put.Parameters) > 0 { @@ -1946,6 +1942,17 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 776dbea0..27c8e436 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -24,7 +24,6 @@ import ( "net/http" "os" "strings" - //"google.golang.org/appengine" ) // Parses a directory with a Dockerfile into a tar for Docker images.. diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 6e7bd0b4..1595ba82 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -62,16 +62,10 @@ import ( // githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" // Web - // "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/patrickmn/go-cache" "google.golang.org/grpc" http2 "gopkg.in/src-d/go-git.v4/plumbing/transport/http" - // Old items (cloud) - // "google.golang.org/appengine" - // "google.golang.org/appengine/memcache" - // applog "google.golang.org/appengine/log" - //cloudrun "google.golang.org/api/run/v1" - "github.com/patrickmn/go-cache" ) // This is used to handle onprem vs offprem databases etc @@ -5915,7 +5909,7 @@ func getOpenapi(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("[INFO] API LENGTH GET: %d, ID: %s", len(parsedApi.Body), id) + log.Printf("[INFO] API LENGTH GET FOR OPENAPI %s: %d, ID: %s", id, len(parsedApi.Body), id) parsedApi.Success = true data, err := json.Marshal(parsedApi) @@ -6579,7 +6573,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { Body: string(body), } - log.Printf("[INFO] API LENGTH: %d, ID: %s", len(parsed.Body), newmd5) + log.Printf("[INFO] API LENGTH FOR %s: %d, ID: %s", api.Name, len(parsed.Body), newmd5) // FIXME: Might cause versioning issues if we re-use the same!! // FIXME: Need a way to track different versions of the same app properly. // Hint: Save API.id somewhere, and use newmd5 to save latest version diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index f2882d9a..78ef796e 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2188,8 +2188,6 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // Here to check access rights ctx := context.Background() - log.Println("GetWorkflow start") - tmpworkflow, err := getWorkflow(ctx, fileId) if err != nil { log.Printf("Failed getting the workflow locally (save workflow): %s", err) @@ -2198,8 +2196,6 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - log.Println("GetWorkflow end") - // FIXME - have a check for org etc too.. if user.Id != tmpworkflow.Owner && user.Role != "admin" { log.Printf("Wrong user (%s) for workflow %s (save)", user.Username, tmpworkflow.ID) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 76ad8d89..f606150e 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2832,7 +2832,6 @@ const AngularWorkflow = (props) => { } var exampledata = item.example === undefined ? "" : item.example - console.log("EXAMPLE: ", exampledata) // Find previous execution and their variables //exampledata === "" && if (workflowExecutions.length > 0) { @@ -3485,10 +3484,11 @@ const AngularWorkflow = (props) => { // Handles the fields under OpenAPI body to be parsed. if (data.name.startsWith("${") && data.name.endsWith("}")) { + console.log("INSIDE VALUE REPLACE: ", data.name, toComplete) // PARAM FIX - Gonna use the ID field, even though it's a hack const paramcheck = selectedAction.parameters.find(param => param.name === "body") if (paramcheck !== undefined) { - if (paramcheck["value_replace"] === undefined) { + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { paramcheck["value_replace"] = [{ "key": data.name, "value": toComplete, @@ -4559,14 +4559,14 @@ const AngularWorkflow = (props) => {
    - - + +
    @@ -5205,7 +5205,10 @@ const AngularWorkflow = (props) => { })} } - {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : Explore selected workflow} + {workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null : + workflow.triggers[selectedTriggerIndex].parameters[0].value === props.match.params.key ? null : + Explore selected workflow + }
    @@ -6935,9 +6938,7 @@ const AngularWorkflow = (props) => { -
    { - //event.preventDefault() - }}> +
    {curapp === null ? null : {selectedResult.app_name}} From f88d88ba9e990f45251a53799688bb175a19d23d Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 4 Mar 2021 17:06:25 +0100 Subject: [PATCH 148/185] BUG: Minor app sdk and build fixes --- backend/app_sdk/app_base.py | 20 +++++++++++--------- backend/go-app/codegen.go | 9 +++++++-- frontend/src/views/AngularWorkflow.jsx | 4 +++- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 748ba455..a4d95d19 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -391,7 +391,8 @@ class AppBase: print("Ret length: %d" % len(ret)) if len(ret) == 1: - ret = ret[0] + #ret = ret[0] + print("DONT make list of 1 into 0!!") print("Return from execution: %s" % ret) if ret == None: @@ -418,7 +419,8 @@ class AppBase: results.append(ret) if len(results) == 1: - results = results[0] + #results = results[0] + print("DONT MAKE LIST FROM 1 TO 0!!") print("\nLOOP: %s\nRESULTS: %s" % (loop_wrapper, results)) return results @@ -521,11 +523,11 @@ class AppBase: data["filename"] = curfile["filename"] filename = curfile["filename"] except KeyError as e: - print("KeyError in file setup: %s" % e) + print(f"KeyError in file setup: {e}") pass ret = requests.post("%s%s" % (self.url, create_path), headers=headers, json=data) - print("Ret CREATE: %s" % ret.text) + print(f"Ret CREATE: {ret.text}") cur_id = "" if ret.status_code == 200: print("RET: %s" % ret.text) @@ -546,7 +548,7 @@ class AppBase: continue new_headers = { - "Authorization": "Bearer %s" % self.authorization, + "Authorization": f"Bearer {self.authorization}", } upload_path = "/api/v1/files/%s/upload?execution_id=%s" % (cur_id, full_execution["execution_id"]) @@ -1255,16 +1257,16 @@ class AppBase: self.logger.info("Checking %s %s %s" % (sourcevalue, check, destinationvalue)) if check == "=" or check.lower() == "equals": - if sourcevalue.lower() == destinationvalue.lower(): + if str(sourcevalue).lower() == str(destinationvalue).lower(): return True elif check == "!=" or check.lower() == "does not equal": - if sourcevalue.lower() != destinationvalue.lower(): + if str(sourcevalue).lower() != str(destinationvalue).lower(): return True elif check.lower() == "startswith": - if sourcevalue.lower().startswith(destinationvalue.lower()): + if str(sourcevalue).lower().startswith(str(destinationvalue).lower()): return True elif check.lower() == "endswith": - if sourcevalue.lower().endswith(destinationvalue.lower()): + if str(sourcevalue).lower().endswith(str(destinationvalue).lower()): return True elif check.lower() == "contains": if destinationvalue.lower() in sourcevalue.lower(): diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index eecc0ee7..9644ccd3 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -392,7 +392,11 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet %s %s %s - return requests.%s(url, headers=headers%s%s%s%s).text + ret = requests.%s(url, headers=headers%s%s%s%s) + try: + return ret.json() + except json.decoder.JSONDecodeError: + return ret.text `, functionname, authenticationParameter, @@ -419,6 +423,7 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet ) // Use lowercase when checking + log.Println(data) /* if strings.Contains(functionname, "filter") { //log.Printf("FUNCTION: %s", data) @@ -1564,7 +1569,7 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo newName := string(fmt.Sprintf("%s", string(fieldname))) if newName[0] == 0x22 && newName[len(newName)-1] == 0x22 { parsedName := newName[1 : len(newName)-1] - log.Printf("Parse name: %s", parsedName) + //log.Printf("[INFO] Parse name: %s", parsedName) fileField = parsedName curParam := WorkflowAppActionParameter{ diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index f606150e..6dd31e3c 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2700,6 +2700,8 @@ const AngularWorkflow = (props) => { selectedAction.label = event.target.value setSelectedAction(selectedAction) + console.log("SHOULD CHANGE NAME EVERYWHERE ITS USED TOO BASED ON OLD NAME!") + /* if (nodeaction.label !== curaction.label) { console.log("BEACH!") @@ -7032,7 +7034,7 @@ const AngularWorkflow = (props) => { Execution Variable - Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more here + Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more here setNewVariableName(event.target.value)} color="primary" From 7a4c921695a2676db69aa640024f5ada4554bfe6 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 6 Mar 2021 13:38:36 +0100 Subject: [PATCH 149/185] Fixed admin issues with cloud --- backend/go-app/codegen.go | 1 - frontend/src/components/Header.js | 3 +- frontend/src/views/Admin.jsx | 60 ++++++++++++++++++------------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 9644ccd3..a9b4c1b6 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -423,7 +423,6 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet ) // Use lowercase when checking - log.Println(data) /* if strings.Contains(functionname, "filter") { //log.Printf("FUNCTION: %s", data) diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index c7cb5e6c..eb6d6e0c 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -151,9 +151,10 @@ const Header = props => { - const logoCheck = !homePage ? null : null + // Handle top bar or something + const logoCheck = !homePage ? null : null const loginTextBrowser = !isLoggedIn ?
    diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 9df961ac..8b1d3ec5 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -430,11 +430,13 @@ const Admin = (props) => { const submitUser = (data) => { console.log("INPUT: ", data) + setLoginInfo("") // Just use this one? var data = { "username": data.Username, "password": data.Password } var baseurl = globalUrl const url = baseurl + '/api/v1/users/register'; + fetch(url, { method: 'POST', credentials: "include", @@ -446,7 +448,7 @@ const Admin = (props) => { .then(response => response.json().then(responseJson => { if (responseJson["success"] === false) { - setLoginInfo("Error in input: " + responseJson.reason) + setLoginInfo("Error: " + responseJson.reason) } else { setLoginInfo("") setModalOpen(false) @@ -912,11 +914,11 @@ const Admin = (props) => { } else if (newValue === 2) { getAppAuthentication() } else if (newValue === 3) { - getEnvironments() + getFiles() } else if (newValue === 4) { getSchedules() } else if (newValue === 5) { - getFiles() + getEnvironments() } else if (newValue === 6) { getOrgs() } @@ -1157,11 +1159,11 @@ const Admin = (props) => { }, }} > - + Edit user
    {
    + @@ -1738,7 +1747,7 @@ const Admin = (props) => { { { fullWidth onChange={(e) => { console.log("VALUE: ", e.target.value) - setUser(data.id, "role", e.target.value) + + if (isCloud) { + setUser(data.username, "role", e.target.value) + } else { + setUser(data.id, "role", e.target.value) + } }} style={{ backgroundColor: theme.palette.surfaceColor, color: "white", height: "50px" }} > @@ -1795,33 +1809,29 @@ const Admin = (props) => { User } - style = {{ minWidth: 150, maxWidth: 150}} + style ={{ minWidth: 135, maxWidth: 135, marginRight: 15,}} /> - + + - + ) })} @@ -1867,7 +1877,7 @@ const Admin = (props) => { uploadFiles(files) } - const filesView = curTab === 5 ? + const filesView = curTab === 3 ? 1366 ? 1366 : 1200, margin: "auto", padding: 20 }} onDrop={uploadFile}>
    @@ -2311,7 +2321,7 @@ const Admin = (props) => {
    : null - const environmentView = curTab === 3 ? + const environmentView = curTab === 5 ?

    Environments

    @@ -2555,10 +2565,10 @@ const Admin = (props) => { > Organization/> Users /> - {isCloud ? null : App Authentication/>} + App Authentication/> + Files /> + Schedules /> {isCloud ? null : Environments/>} - {isCloud ? null : Schedules />} - {isCloud ? null : Files />} {window.location.protocol == "http:" && window.location.port === "3000" ? Hybrid/> : null} {window.location.protocol == "http:" && window.location.port === "3000" ? Organizations/> : null} {window.location.protocol === "http:" && window.location.port === "3000" ? Categories/> : null} From 32718fbd3bca1822bc9b238f874b1731fc4dce9f Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 6 Mar 2021 15:12:58 +0100 Subject: [PATCH 150/185] Minor fixes to main.go --- backend/go-app/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 1595ba82..a2a5784c 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -663,7 +663,7 @@ func handleApiAuthentication(resp http.ResponseWriter, request *http.Request) (U sessionToken := c.Value session, err := getSession(ctx, sessionToken) if err != nil { - log.Printf("Session %s doesn't exist (session auth): %s", sessionToken, err) + log.Printf("[WARNING] Session %s doesn't exist (session auth): %s", sessionToken, err) return User{}, err } @@ -1286,7 +1286,7 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { //sessionToken = c.Value //session, err := getSession(ctx, sessionToken) //if err != nil { - // log.Printf("Session %s doesn't exist (logout): %s", sessionToken, err) + // log.Printf("[WARNING] Session %s doesn't exist (logout): %s", sessionToken, err) // resp.WriteHeader(401) // resp.Write([]byte(`{"success": false, "reason": "Couldn't find your session"}`)) // return @@ -2471,7 +2471,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - log.Printf("Username: %s", data.Username) + log.Printf("[INFO] Login Username: %s", data.Username) q := datastore.NewQuery("Users").Filter("Username =", data.Username) var users []User _, err = dbclient.GetAll(ctx, q, &users) @@ -2509,7 +2509,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { // FIXME - have timeout here loginData := `{"success": true}` if len(Userdata.Session) != 0 { - log.Println("User session exists - resetting") + log.Println("[INFO] User session exists - resetting") expiration := time.Now().Add(3600 * time.Second) http.SetCookie(resp, &http.Cookie{ @@ -7204,7 +7204,7 @@ func runInit(ctx context.Context) { } else { if len(users) < 5 && len(users) > 0 { for _, user := range users { - log.Printf("Username: %s, role: %s", user.Username, user.Role) + log.Printf("[INFO] Username: %s, role: %s", user.Username, user.Role) } } else { log.Printf("Found %d users.", len(users)) From 00d0f444e559ca1c0fbbeb3892b86c47c8babcec Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 6 Mar 2021 15:27:44 +0100 Subject: [PATCH 151/185] Minor fixes to the upload view --- frontend/src/views/AppCreator.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index afb22314..b7d4ebdb 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -319,6 +319,8 @@ const AppCreator = (props) => { const checkQuery = () => { var urlParams = new URLSearchParams(window.location.search) if (!urlParams.has("id")) { + setActionAmount(0) + setIsAppLoaded(true) return } @@ -2105,7 +2107,7 @@ const AppCreator = (props) => { : null}
    -

    Actions ({actionAmount} / {actions.length})

    +

    Actions {actionAmount > 0 ? ({actionAmount} / {actions.length}) : null}

    Actions are the tasks performed by an app. Read more about actions and apps here.
    @@ -2199,7 +2201,9 @@ const AppCreator = (props) => { const imageInfo = - const alternateImg = + const alternateImg = { + upload.click() + }}/> const zoomIn = () => { setScale(scale+0.1); From 67373bfe3e6ffe50bf9aedaf7d876d33097840ef Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 6 Mar 2021 18:23:48 +0100 Subject: [PATCH 152/185] Added eradication to AppCreator --- frontend/src/views/AppCreator.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 5432a748..036cc4be 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2015,8 +2015,10 @@ const AppCreator = (props) => { "SIEM", "Network", "Assets", + "Eradication", "Other", ] + const tagView =
    {/* From beeba414bc2606c68486fe67ca3edd15c85a4590 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 7 Mar 2021 11:56:33 +0100 Subject: [PATCH 153/185] Added automated authentication selection if it exists --- backend/app_sdk/app_base.py | 25 ++++++------ backend/go-app/walkoff.go | 37 +++++++++++++----- frontend/src/views/AngularWorkflow.jsx | 53 +++++++++++++++++++++++++- frontend/src/views/AppCreator.jsx | 6 +-- 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index a4d95d19..219506b7 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1447,7 +1447,7 @@ class AppBase: params = {} try: for item in action["authentication"]: - print("AUTH: ", key, value) + #print("AUTH: ", key, value) params[item["key"]] = item["value"] except KeyError: print("No authentication specified!") @@ -1469,11 +1469,11 @@ class AppBase: if "||" in parameter["value"]: splitvalue = parameter["value"].split("||") if len(splitvalue) > 1: - print(f'[INFO] Parsed split || options of actions["parameters"]["name"]') + #print(f'[INFO] Parsed split || options of actions["parameters"]["name"]') action["parameters"][counter]["value"] = splitvalue[1] except (IndexError, KeyError, TypeError) as e: - print("Options err: {e}") + print("[WARNING] Options err: {e}") # This part is purely for OpenAPI accessibility. # It replaces the data back into the main item @@ -1487,12 +1487,12 @@ class AppBase: if values != None: added = 0 for val in values: - print(f"VAL: {val}") + #print(f"VAL: {val}") #parameter["value"].replace(val["key"], val["value"], -1) - print(f'PARAM1: {action["parameters"][counter]["value"]}') + #print(f'PARAM1: {action["parameters"][counter]["value"]}') action["parameters"][counter]["value"] = action["parameters"][counter]["value"].replace(val["key"], val["value"], 1) #action["parameters"][counter]["value"].replace(r"${url}", r"$Find_URLs.valid.#.data", 1) - print(f'PARAM2: {action["parameters"][counter]["value"]}') + #print(f'PARAM2: {action["parameters"][counter]["value"]}') #newparams.append({ # "name": val["key"], # "value": val["value"], @@ -1503,7 +1503,8 @@ class AppBase: # }, #}) - print(f'[INFO] Added param {val["key"]} for body with value {val["value"]} (using OpenAPI)') + #print(f'[INFO] Added param {val["key"]} for body with value {val["value"]} (using OpenAPI)') + print(f'[INFO] Added param {val["key"]} for body (using OpenAPI)') added += 1 #action["parameters"]["body"] @@ -1515,7 +1516,7 @@ class AppBase: break - print(action["parameters"]) + #print(action["parameters"]) for parameter in newparams: action["parameters"].append(parameter) @@ -1841,10 +1842,10 @@ class AppBase: #print(f"Error with subparam deletion of {subparam} in {multi_parameters} (2)") pass - print() - print(f"Param: {params}") - print(f"Multiparams: {multi_parameters}") - print() + #print() + #print(f"Param: {params}") + #print(f"Multiparams: {multi_parameters}") + #print() if not multiexecution: #newparams.append({ diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 78ef796e..c48c52c3 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -6103,6 +6103,11 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] Starting app hotloading") + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") + requestCache.Delete(cacheKey) + // Just need to be logged in // FIXME - should have some permissions? user, err := handleApiAuthentication(resp, request) @@ -6135,6 +6140,11 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { return } + cacheKey = fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") + requestCache.Delete(cacheKey) + resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) } @@ -6737,7 +6747,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin if len(removeApps) > 0 { for _, item := range removeApps { - log.Printf("[WARNING] Removing duplicate: %s", item) + log.Printf("[WARNING] Removing duplicate app: %s", item) err = DeleteKey(ctx, "workflowapp", item) if err != nil { log.Printf("[ERROR] Failed deleting duplicate %s: %s", item, err) @@ -6758,15 +6768,17 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin continue } - err = increaseStatisticsField(ctx, "total_apps_created", workflowapp.ID, 1, "") - if err != nil { - log.Printf("Failed to increase total apps created stats: %s", err) - } + /* + err = increaseStatisticsField(ctx, "total_apps_created", workflowapp.ID, 1, "") + if err != nil { + log.Printf("Failed to increase total apps created stats: %s", err) + } - err = increaseStatisticsField(ctx, "total_apps_loaded", workflowapp.ID, 1, "") - if err != nil { - log.Printf("Failed to increase total apps loaded stats: %s", err) - } + err = increaseStatisticsField(ctx, "total_apps_loaded", workflowapp.ID, 1, "") + if err != nil { + log.Printf("Failed to increase total apps loaded stats: %s", err) + } + */ //log.Printf("Added %s:%s to the database", workflowapp.Name, workflowapp.AppVersion) @@ -6801,6 +6813,12 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin return buildLaterFirst, buildLaterList, err } + // This is getting silly + cacheKey := fmt.Sprintf("workflowapps-sorted-100") + requestCache.Delete(cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-500") + requestCache.Delete(cacheKey) + //log.Printf("BUILDLATERFIRST: %d, BUILDLATERLIST: %d", len(buildLaterFirst), len(buildLaterList)) if len(extra) == 0 { log.Printf("[INFO] Starting build of %d containers (FIRST)", len(buildLaterFirst)) @@ -6811,6 +6829,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin } else { if len(item.Tags) > 0 { log.Printf("[INFO] Successfully built image %s", item.Tags[0]) + } else { log.Printf("[INFO] Successfully built Docker image") } diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 6dd31e3c..14e7d5af 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2450,7 +2450,8 @@ const AngularWorkflow = (props) => { authentication: [], execution_variable: undefined, example: example, - category: app.categories !== null && app.categories !== undefined && app.categories.length > 0 ? app.categories[0] : "" + category: app.categories !== null && app.categories !== undefined && app.categories.length > 0 ? app.categories[0] : "", + authentication_id: "", } // FIXME: overwrite category if the ACTION chosen has a different category @@ -2511,6 +2512,56 @@ const AngularWorkflow = (props) => { console.log("SHOULD STITCH WITH STARTNODE") cy.add(edgeToBeAdded) } + + // AUTHENTICATION + if (app.authentication.required) { + // Setup auth here :) + const authenticationOptions = [] + var findAuthId = "" + if (newAppData.authentication_id !== null && newAppData.authentication_id !== undefined && newAppData.authentication_id.length > 0) { + findAuthId = newAppData.authentication_id + } + + var tmpAuth = JSON.parse(JSON.stringify(appAuthentication)) + for (var key in tmpAuth) { + var item = tmpAuth[key] + + const newfields = {} + for (var filterkey in item.fields) { + newfields[item.fields[filterkey].key] = item.fields[filterkey].value + } + + item.fields = newfields + if (item.app.name === app.name) { + authenticationOptions.push(item) + if (item.id === findAuthId) { + newAppData.selectedAuthentication = item + } + } + } + + if (authenticationOptions !== undefined && authenticationOptions !== null && authenticationOptions.length > 0) { + for (var key in authenticationOptions) { + const option = authenticationOptions[key] + if (option.active) { + newAppData.selectedAuthentication = option + newAppData.authentication_id = option.id + break + } + } + } + + //newAppData.authentication = authenticationOptions + //if (newAppData.selectedAuthentication === null || newAppData.selectedAuthentication === undefined || newAppData.selectedAuthentication.length === "") { + // newAppData.selectedAuthentication = {} + //} else { + // console.log("CAN WE SELECT AUTH?: ", authenticationOptions) + //} + } else { + newAppData.authentication = [] + newAppData.authentication_id = "" + newAppData.selectedAuthentication = {} + } workflow.actions.push(newAppData) setWorkflow(workflow) diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 036cc4be..e4ae6b40 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2010,11 +2010,11 @@ const AppCreator = (props) => { const categories = [ "Communication", "Cases", - "EDR", - "Intel", "SIEM", - "Network", "Assets", + "Intel", + "IAM", + "Network", "Eradication", "Other", ] From 62ca9e932172d06fb67483734330932941f04d18 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 7 Mar 2021 16:57:25 +0100 Subject: [PATCH 154/185] Set branch/condition failures to SKIPPED --- backend/app_sdk/app_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 219506b7..1cb3dc80 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1404,7 +1404,7 @@ class AppBase: if not branchcheck: self.logger.info("Failed one or more branch conditions.") action_result["result"] = tmpresult - action_result["status"] = "FAILURE" + action_result["status"] = "SKIPPED" try: ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result) self.logger.info("Result: %d" % ret.status_code) @@ -1430,7 +1430,7 @@ class AppBase: try: func = getattr(self, actionname, None) if func == None: - self.logger.debug("Failed executing %s because func is None." % actionname) + self.logger.debug(f"Failed executing {actionname} because func is None.") action_result["status"] = "FAILURE" action_result["result"] = "Function %s doesn't exist." % actionname elif callable(func): From c7ba0a5a138f6629089bec041a81a3e4a8bb75d8 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 7 Mar 2021 16:58:08 +0100 Subject: [PATCH 155/185] #279: Changed codegen to be based on params --- backend/app_sdk/build.sh | 2 +- backend/go-app/codegen.go | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 25fa59d6..ffed66c5 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.60 +VERSION=0.8.61 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index a9b4c1b6..71802bf4 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -260,9 +260,14 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet queryString += ", " } + /* + queryData += fmt.Sprintf(` + if %s: + url += f"&%s={%s}"`, query, query, query) + */ queryData += fmt.Sprintf(` if %s: - url += f"&%s={%s}"`, query, query, query) + params["%s"] = %s`, query, query, query) } } else { //log.Printf("No optional queries?") @@ -384,6 +389,7 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet // Extra param for authentication scheme(s) // The last weird one is the body.. Tabs & spaces sucks. data := fmt.Sprintf(` async def %s(self%s%s%s%s%s%s%s): + params={} %s url=f"%s%s" %s @@ -392,7 +398,7 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet %s %s %s - ret = requests.%s(url, headers=headers%s%s%s%s) + ret = requests.%s(url, headers=headers, params=params%s%s%s%s) try: return ret.json() except json.decoder.JSONDecodeError: @@ -423,13 +429,11 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet ) // Use lowercase when checking - /* - if strings.Contains(functionname, "filter") { - //log.Printf("FUNCTION: %s", data) - log.Println(data) - log.Printf("Queries: %s", queryString) - } - */ + if strings.Contains(functionname, "login") { + //log.Printf("FUNCTION: %s", data) + log.Println(data) + log.Printf("Queries: %s", queryString) + } //log.Printf(data) return functionname, data From db1abbd0518d65ecbd28b543174cb60375e0b02f Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 8 Mar 2021 11:33:58 +0100 Subject: [PATCH 156/185] Minor changes for 0.8.61 --- docker-compose.yml | 8 ++++---- functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/orborus.go | 2 +- functions/onprem/worker/build.sh | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index cc260615..d50df814 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.60 + image: ghcr.io/frikky/shuffle-frontend:0.8.61 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.60 + image: ghcr.io/frikky/shuffle-backend:0.8.61 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -47,7 +47,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.60 + image: ghcr.io/frikky/shuffle-orborus:0.8.61 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -56,7 +56,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - SHUFFLE_APP_SDK_VERSION=0.8.60 - - SHUFFLE_WORKER_VERSION=0.8.60 + - SHUFFLE_WORKER_VERSION=0.8.61 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index a0fcec2c..3156eee0 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.60 +VERSION=0.8.61 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 30d6394a..4323426b 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -259,7 +259,7 @@ func initializeImages() { log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.8.60" + workerVersion = "0.8.61" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 907f053e..10eb7b03 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-worker -VERSION=0.8.60 +VERSION=0.8.61 echo "Running docker build with $NAME:$VERSION" #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . From 5e1ddc60fdc7b1fc3c4eecb11a6f067e664d8e8a Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 10 Mar 2021 10:35:21 +0100 Subject: [PATCH 157/185] Fixed worker issues --- backend/go-app/codegen.go | 12 +-- backend/go-app/main.go | 1 + backend/go-app/walkoff.go | 10 ++- frontend/src/views/AngularWorkflow.jsx | 4 +- functions/onprem/worker/worker.go | 100 ++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 71802bf4..4389219f 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -429,11 +429,13 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet ) // Use lowercase when checking - if strings.Contains(functionname, "login") { - //log.Printf("FUNCTION: %s", data) - log.Println(data) - log.Printf("Queries: %s", queryString) - } + /* + if strings.Contains(functionname, "login") { + //log.Printf("FUNCTION: %s", data) + log.Println(data) + log.Printf("Queries: %s", queryString) + } + */ //log.Printf(data) return functionname, data diff --git a/backend/go-app/main.go b/backend/go-app/main.go index a2a5784c..cc620eed 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -8429,6 +8429,7 @@ func initHandlers() { // Orgs r.HandleFunc("/api/v1/orgs", handleGetOrgs).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/orgs/", handleGetOrgs).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", handleGetOrg).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") //r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index c48c52c3..c8acad84 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -7056,8 +7056,12 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { } if err != nil { - log.Printf("Cursorerror: %s", err) - break + if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { + log.Printf("[WARNING] Cursorerror in app grab WARNING: %s", err) + } else { + log.Printf("[ERROR] Cursorerror in app grab: %s", err) + break + } } else { //log.Printf("NEXTCURSOR: %s", nextCursor) nextStr := fmt.Sprintf("%s", nextCursor) @@ -7115,7 +7119,7 @@ func getAllSchedules(ctx context.Context, orgId string) ([]ScheduleOld, error) { //FIXME: Add cursor func getAllWorkflowApps(ctx context.Context, maxLen int) ([]WorkflowApp, error) { var apps []WorkflowApp - query := datastore.NewQuery("workflowapp").Order("-edited").Limit(20) + query := datastore.NewQuery("workflowapp").Order("-edited").Limit(10) //query := datastore.NewQuery("workflowapp").Order("-edited").Limit(40) cacheKey := fmt.Sprintf("workflowapps-sorted-%d", maxLen) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 14e7d5af..f328fbb7 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -342,7 +342,7 @@ const AngularWorkflow = (props) => { setAuthenticationModalOpen(false) // Needs a refresh with the new authentication.. - alert.success("Successfully saved new app auth") + //alert.success("Successfully saved new app auth") } }) .catch(error => { @@ -1582,7 +1582,7 @@ const AngularWorkflow = (props) => { if (elements.length === 0 && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) { setGraphSetup(true) setupGraph() - } else if (!established && cy !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0){ + } else if (!established && cy !== undefined && apps !== null && apps !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0){ setEstablished(true) cy.edgehandles({ handleNodes: (el) => el.isNode(), diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index f3d9250a..02718009 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -19,6 +19,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + //"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/mount" dockerclient "github.com/docker/docker/client" @@ -776,7 +777,7 @@ type AppExecutionExample struct { // removes every container except itself (worker) func shutdown(workflowExecution WorkflowExecution, nodeId string, reason string, handleResultSend bool) { - log.Printf("[INFO] Shutdown started") + log.Printf("[INFO] Shutdown started with reason %s", reason) //reason := "Error in execution" sleepDuration := 1 @@ -881,6 +882,7 @@ func shutdown(workflowExecution WorkflowExecution, nodeId string, reason string, // Deploys the internal worker whenever something happens func deployApp(cli *dockerclient.Client, image string, identifier string, env []string) error { // form basic hostConfig + ctx := context.Background() hostConfig := &container.HostConfig{ LogConfig: container.LogConfig{ Type: "json-file", @@ -942,7 +944,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] } cont, err := cli.ContainerCreate( - context.Background(), + ctx, config, hostConfig, nil, @@ -955,7 +957,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] return err } - err = cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) + err = cli.ContainerStart(ctx, cont.ID, types.ContainerStartOptions{}) if err != nil { log.Printf("[ERROR] Failed to start container in environment %s: %s", environment, err) //shutdown(workflowExecution, workflowExecution.Workflow.ID, true) @@ -963,6 +965,73 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] } log.Printf("[INFO] Container %s was created for %s", cont.ID, identifier) + + // Waiting to see if it exits.. Stupid, but stable(r) + time.Sleep(2 * time.Second) + + stats, err := cli.ContainerInspect(ctx, cont.ID) + if err != nil { + log.Printf("[ERROR] Failed getting container stats") + } else { + //log.Printf("[INFO] Info for container: %#v", stats) + //log.Printf("%#v", stats.Config) + //log.Printf("%#v", stats.ContainerJSONBase.State) + log.Printf("STATUS: %s", stats.ContainerJSONBase.State.Status) + if stats.ContainerJSONBase.State.Status == "exited" { + logOptions := types.ContainerLogsOptions{ + ShowStdout: true, + } + + out, err := cli.ContainerLogs(ctx, cont.ID, logOptions) + if err != nil { + log.Printf("[INFO] Failed getting logs: %s", err) + } else { + log.Printf("IN ELSE FOR DEPLOY") + buf := new(strings.Builder) + io.Copy(buf, out) + logs := buf.String() + log.Printf("Logs: %s", logs) + //log.Printf(logs) + // check errors + /* + if strings.Contains(logs, "Error") { + log.Printf("ERROR IN %s?", cont.ID) + log.Println(logs) + //return errors.New(fmt.Sprintf("ERROR FROM CONTAINER %s", cont.ID)) + } else { + log.Printf("NORMAL EXEC OF %s?", cont.ID) + } + */ + } + + log.Printf("ERROR IN CONTAINER DEPLOYMENT - ITS EXITED!") + + return errors.New(fmt.Sprintf(`{"success": false, "reason": "Container %s exited prematurely.","debug": "docker logs -f %s"}`, cont.ID, cont.ID)) + } + } + + /* + //log.Printf("%#v", stats.Config.Status) + //ContainerJSONtoConfig(cj dockType.ContainerJSON) ContainerConfig { + listOptions := types.ContainerListOptions{ + Filters: filters.Args{ + map[string][]string{"ancestor": {":"}}, + }, + } + containers, err := cli.ContainerList(ctx, listOptions) + */ + + //log.Printf("%#v", cont.Status) + //config := ContainerJSONtoConfig(stats) + //log.Printf("CONFIG: %#v", config) + + /* + logOptions := types.ContainerLogsOptions{ + ShowStdout: true, + } + + */ + containerIds = append(containerIds, cont.ID) return nil } @@ -1570,6 +1639,10 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { if cleanupEnv == "true" { err = deployApp(dockercli, images[0], identifier, env) if err != nil { + if strings.Contains(err.Error(), "exited prematurely") { + shutdown(workflowExecution, action.ID, err.Error(), true) + } + log.Printf("[WARNING] Failed CLEANUP execution. Downloading image remotely.") reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) if err != nil { @@ -1595,6 +1668,10 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { if err != nil { log.Printf("[ERROR] Failed deploying image for the FOURTH time. Aborting if the image doesn't exist") + if strings.Contains(err.Error(), "exited prematurely") { + shutdown(workflowExecution, action.ID, err.Error(), true) + } + if strings.Contains(err.Error(), "No such image") { //log.Printf("[WARNING] Failed deploying %s from image %s: %s", identifier, image, err) log.Printf("[ERROR] Image doesn't exist. Shutting down") @@ -1606,6 +1683,10 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { err = deployApp(dockercli, image, identifier, env) if err != nil { + if strings.Contains(err.Error(), "exited prematurely") { + shutdown(workflowExecution, action.ID, err.Error(), true) + } + // Trying to replace with lowercase to deploy again. This seems to work with Dockerhub well. // FIXME: Should try to remotely download directly if this persists. image = fmt.Sprintf("%s:%s_%s", baseimagename, strings.ToLower(action.AppName), action.AppVersion) @@ -1615,6 +1696,10 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { err = deployApp(dockercli, image, identifier, env) if err != nil { + if strings.Contains(err.Error(), "exited prematurely") { + shutdown(workflowExecution, action.ID, err.Error(), true) + } + image = fmt.Sprintf("%s/%s:%s_%s", registryName, baseimagename, strings.ToLower(action.AppName), action.AppVersion) if strings.Contains(image, " ") { image = strings.ReplaceAll(image, " ", "-") @@ -1622,6 +1707,10 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { err = deployApp(dockercli, image, identifier, env) if err != nil { + if strings.Contains(err.Error(), "exited prematurely") { + shutdown(workflowExecution, action.ID, err.Error(), true) + } + log.Printf("[WARNING] Failed deploying image THRICE. Attempting to download the latter as last resort.") reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) if err != nil { @@ -1645,8 +1734,11 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { err = deployApp(dockercli, image, identifier, env) if err != nil { - log.Printf("[ERROR] Failed deploying image for the FOURTH time. Aborting if the image doesn't exist") + if strings.Contains(err.Error(), "exited prematurely") { + shutdown(workflowExecution, action.ID, err.Error(), true) + } + if strings.Contains(err.Error(), "No such image") { //log.Printf("[WARNING] Failed deploying %s from image %s: %s", identifier, image, err) log.Printf("[ERROR] Image doesn't exist. Shutting down") From 6c09375f33d56b45d49ca889ec3217603ba6c3e4 Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 10 Mar 2021 17:16:55 +0100 Subject: [PATCH 158/185] Workflow completion fixes --- backend/go-app/walkoff.go | 38 ++++++++++++++++-- docker-compose.yml | 8 ++-- frontend/src/views/Admin.jsx | 12 ++++-- frontend/src/views/AngularWorkflow.jsx | 6 +++ functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/orborus.go | 2 +- functions/onprem/worker/Dockerfile | 10 ++--- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 55 +++++++++++++++----------- 9 files changed, 94 insertions(+), 41 deletions(-) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index c8acad84..8f28f72e 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -17,6 +17,9 @@ import ( "strings" "time" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "cloud.google.com/go/datastore" scheduler "cloud.google.com/go/scheduler/apiv1" gyaml "github.com/ghodss/yaml" @@ -977,6 +980,21 @@ func validateNewWorkerExecution(body []byte) error { return errors.New(fmt.Sprintf("Bad length of trigger: %d (probably normal app)", len(execution.Workflow.Triggers))) } + if execution.Status == "EXECUTING" { + log.Printf("[INFO] Inside executing.") + extra := 0 + for _, trigger := range execution.Workflow.Triggers { + //log.Printf("Appname trigger (0): %s", trigger.AppName) + if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" { + extra += 1 + } + } + + if len(execution.Workflow.Actions)+extra == len(execution.Results) { + execution.Status = "FINISHED" + } + } + // FIXME: Add extra here //executionLength := len(baseExecution.Workflow.Actions) //if executionLength != len(execution.Results) { @@ -986,7 +1004,7 @@ func validateNewWorkerExecution(body []byte) error { //log.Printf("\n\nSHOULD SET BACKEND DATA FOR EXEC \n\n") err = setWorkflowExecution(ctx, execution, true) if err == nil { - log.Printf("[INFO] Set workflowexecution based on new worker (>0.8.53) for execution %s", baseExecution.ExecutionId) + log.Printf("[INFO] Set workflowexecution based on new worker (>0.8.53) for execution %s. Actions: %d, Triggers: %d, Results: %d", execution.ExecutionId, len(execution.Workflow.Actions), len(execution.Workflow.Triggers), len(execution.Results)) //log.Printf("[INFO] Successfully set the execution to wait.") } else { log.Printf("[WARNING] Failed to set the execution to wait.") @@ -3106,7 +3124,7 @@ func cleanupExecutions(resp http.ResponseWriter, request *http.Request) { user, err := handleApiAuthentication(resp, request) if err != nil { - log.Printf("Api authentication failed in execute workflow: %s", err) + log.Printf("[INFO] Api authentication failed in cleanup executions: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "message": "Not authenticated"}`)) return @@ -3871,7 +3889,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { user, err := handleApiAuthentication(resp, request) if err != nil { - log.Printf("Api authentication failed in execute workflow: %s", err) + log.Printf("[INFO] Api authentication failed in execute workflow: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -6549,8 +6567,20 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin buildLaterFirst := []buildLaterStruct{} buildLaterList := []buildLaterStruct{} - // It's here to prevent getting them in every iteration ctx := context.Background() + if forceUpdate { + dockercli, err := client.NewEnvClient() + if err == nil { + _, err := dockercli.ImagePull(ctx, "frikky/shuffle:app_sdk", types.ImagePullOptions{}) + if err != nil { + log.Printf("[WARNING] Failed to download apps with the new App SDK: %s", err) + } + } else { + log.Printf("[WARNING] Failed to download apps with the new App SDK because of docker cli: %s", err) + } + } + + // It's here to prevent getting them in every iteration for _, file := range dir { if len(onlyname) > 0 && file.Name() != onlyname { continue diff --git a/docker-compose.yml b/docker-compose.yml index d50df814..ff2bf5d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.61 + image: ghcr.io/frikky/shuffle-frontend:0.8.62 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.61 + image: ghcr.io/frikky/shuffle-backend:0.8.62 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -47,7 +47,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.61 + image: ghcr.io/frikky/shuffle-orborus:0.8.62 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -56,7 +56,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - SHUFFLE_APP_SDK_VERSION=0.8.60 - - SHUFFLE_WORKER_VERSION=0.8.61 + - SHUFFLE_WORKER_VERSION=0.8.62 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 8b1d3ec5..7cee4bbe 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -377,6 +377,11 @@ const Admin = (props) => { } const handleGetOrg = (orgId) => { + if (orgId.length === 0) { + alert.error("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.") + return + } + // Just use this one? var baseurl = globalUrl const url = baseurl + '/api/v1/orgs/'+orgId @@ -870,7 +875,8 @@ const Admin = (props) => { }) .then((response) => { if (response.status !== 200) { - window.location.pathname = "/workflows" + // Ahh, this happens because they're not admin + // window.location.pathname = "/workflows" return } @@ -1359,14 +1365,14 @@ const Admin = (props) => { const cancelSubscriptions = (subscription_id) => { console.log(selectedOrganization) + const orgId = selectedOrganization.id const data = { "subscription_id": subscription_id, "action": "cancel", "org_id": selectedOrganization.id, } - - const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + const url = globalUrl + `/api/v1/orgs/${orgId}`; fetch(url, { mode: 'cors', method: 'POST', diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index f328fbb7..3485f90c 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -382,6 +382,12 @@ const AngularWorkflow = (props) => { if (execution !== null && execution !== undefined) { setExecutionData(execution) setExecutionModalView(1) + start() + + setExecutionRequest({ + "execution_id": execution.execution_id, + "authorization": execution.authorization, + }) const newitem = removeParam("execution_id", cursearch) props.history.push(curpath+newitem) diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 3156eee0..4c0b9450 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.61 +VERSION=0.8.62 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 4323426b..faae0c38 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -259,7 +259,7 @@ func initializeImages() { log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.8.61" + workerVersion = "0.8.62" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } diff --git a/functions/onprem/worker/Dockerfile b/functions/onprem/worker/Dockerfile index a95b099e..3bf1541a 100644 --- a/functions/onprem/worker/Dockerfile +++ b/functions/onprem/worker/Dockerfile @@ -5,11 +5,11 @@ WORKDIR /app COPY worker.go /app/worker.go RUN go env -w GO111MODULE=auto -RUN go get -u github.com/docker/docker/api/types -RUN go get -u github.com/docker/docker/api/types/container -RUN go get -u github.com/docker/docker/client -RUN go get -u github.com/gorilla/mux -RUN go get -u github.com/patrickmn/go-cache +RUN go get github.com/docker/docker/api/types +RUN go get github.com/docker/docker/api/types/container +RUN go get github.com/docker/docker/client +RUN go get github.com/gorilla/mux +RUN go get github.com/patrickmn/go-cache RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker . diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 10eb7b03..3f2a483e 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-worker -VERSION=0.8.61 +VERSION=0.8.62 echo "Running docker build with $NAME:$VERSION" #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 02718009..3e15c282 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -777,11 +777,11 @@ type AppExecutionExample struct { // removes every container except itself (worker) func shutdown(workflowExecution WorkflowExecution, nodeId string, reason string, handleResultSend bool) { - log.Printf("[INFO] Shutdown started with reason %s", reason) + log.Printf("[INFO] Shutdown (%s) started with reason %s", workflowExecution.Status, reason) //reason := "Error in execution" sleepDuration := 1 - if handleResultSend { + if handleResultSend && requestsSent < 2 { data, err := json.Marshal(workflowExecution) if err == nil { sendResult(workflowExecution, data) @@ -829,7 +829,7 @@ func shutdown(workflowExecution WorkflowExecution, nodeId string, reason string, //fmt.Println(url.QueryEscape(query)) fullUrl += path - log.Printf("Abort URL: %s", fullUrl) + log.Printf("[INFO] Abort URL: %s", fullUrl) req, err := http.NewRequest( "GET", @@ -976,7 +976,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] //log.Printf("[INFO] Info for container: %#v", stats) //log.Printf("%#v", stats.Config) //log.Printf("%#v", stats.ContainerJSONBase.State) - log.Printf("STATUS: %s", stats.ContainerJSONBase.State.Status) + log.Printf("[INFO] EXECUTION STATUS: %s", stats.ContainerJSONBase.State.Status) if stats.ContainerJSONBase.State.Status == "exited" { logOptions := types.ContainerLogsOptions{ ShowStdout: true, @@ -991,6 +991,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] io.Copy(buf, out) logs := buf.String() log.Printf("Logs: %s", logs) + //log.Printf(logs) // check errors /* @@ -2290,6 +2291,8 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution"}`))) return } + + log.Printf(`[INFO] Got result %s from %s`, actionResult.Status, actionResult.Action.ID) resultLength := len(workflowExecution.Results) dbSave := false setExecution := true @@ -2448,6 +2451,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl for index, item := range workflowExecution.Results { if item.Action.ID == actionResult.Action.ID { found = true + if item.Status == actionResult.Status { skip = true } @@ -2478,38 +2482,45 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl log.Printf("[INFO] Updating %s in workflow %s from %s to %s", actionResult.Action.ID, workflowExecution.ExecutionId, workflowExecution.Results[outerindex].Status, actionResult.Status) workflowExecution.Results[outerindex] = actionResult } else { - log.Printf("[INFO] Setting value of %s in workflow %s to %s", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status) workflowExecution.Results = append(workflowExecution.Results, actionResult) + log.Printf("[INFO] Setting value (1) of %s in execution %s to %s. New result length: %d", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status, len(workflowExecution.Results)) } } else { - log.Printf("[INFO] Setting value of %s in workflow %s to %s", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status) workflowExecution.Results = append(workflowExecution.Results, actionResult) + log.Printf("[INFO] Setting value (2) of %s in execution %s to %s. New result length: %d", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status, len(workflowExecution.Results)) } // FIXME: Have a check for skippednodes and their parents - for resultIndex, result := range workflowExecution.Results { - if result.Status != "SKIPPED" { - continue - } + /* + for resultIndex, result := range workflowExecution.Results { + if result.Status != "SKIPPED" { + continue + } - // Checks if all parents are skipped or failed. Otherwise removes them from the results - for _, branch := range workflowExecution.Workflow.Branches { - if branch.DestinationID == result.Action.ID { - for _, subresult := range workflowExecution.Results { - if subresult.Action.ID == branch.SourceID { - if subresult.Status != "SKIPPED" && subresult.Status != "FAILURE" { - log.Printf("SUBRESULT PARENT STATUS: %s", subresult.Status) - log.Printf("Should remove resultIndex: %d", resultIndex) + // Checks if all parents are skipped or failed. + // Otherwise removes them from the results + for _, branch := range workflowExecution.Workflow.Branches { + if branch.DestinationID == result.Action.ID { + for _, subresult := range workflowExecution.Results { + if subresult.Action.ID == branch.SourceID { + if subresult.Status != "SKIPPED" && subresult.Status != "FAILURE" { + //log.Printf("SUBRESULT PARENT STATUS: %s", subresult.Status) + //log.Printf("Should remove resultIndex: %d", resultIndex) - workflowExecution.Results = append(workflowExecution.Results[:resultIndex], workflowExecution.Results[resultIndex+1:]...) + // FIXME: Reinstate this? + //workflowExecution.Results = append(workflowExecution.Results[:resultIndex], workflowExecution.Results[resultIndex+1:]...) + _ = resultIndex - break + break + } } } } } } - } + + log.Printf("NEW LENGTH: %d", len(workflowExecution.Results)) + */ extraInputs := 0 for _, trigger := range workflowExecution.Workflow.Triggers { @@ -2697,7 +2708,7 @@ func sendResult(workflowExecution WorkflowExecution, data []byte) { } func validateFinished(workflowExecution WorkflowExecution) { - log.Printf("[INFO] Status: %s, Actions: %d, Extra: %d, Results: %d\n", workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results)) + log.Printf("[INFO] VALIDATION. Status: %s, Actions: %d, Extra: %d, Results: %d\n", workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results)) //if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions)+extra { if (len(environments) == 1 && requestsSent == 0 && len(workflowExecution.Results) >= 1) || (len(workflowExecution.Results) >= len(workflowExecution.Workflow.Actions) && len(workflowExecution.Workflow.Actions) > 0) { From 61f51592354952989a619c5cafc4dfcca1dea751 Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 10 Mar 2021 17:33:17 +0100 Subject: [PATCH 159/185] Fixed a bug with Workers not getting backend access because of Kubernetes changes --- functions/onprem/orborus/orborus.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index faae0c38..015d2986 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -101,7 +101,7 @@ func getThisContainerId() { log.Printf("[INFO] Running containerized in Docker!") default: - fCol = "0" // for backward-compatibility with production + fCol = "3" // for backward-compatibility with production log.Printf("[WARNING] RUNNING_MODE not set - defaulting to Docker (NOT Kubernetes).") } @@ -119,14 +119,14 @@ func getThisContainerId() { //docker-76c537e9a4b7c7233011f5d70e6b7f2d600b6413ac58a96519b8dca7a3f7117a.scope } } else { - if fCol != "0" { + if fCol == "0" { containerId = "shuffle-orborus" log.Printf("[WARNING] Failed getting container ID: %s", err) } } } - log.Printf("Started with containerId %s", containerId) + log.Printf(`[INFO] Started with containerId "%s"`, containerId) } // Deploys the internal worker whenever something happens From 2a825dd1ea66e7482a68b1d837eccb57773a2d26 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 11 Mar 2021 16:50:58 +0100 Subject: [PATCH 160/185] #293: Entire fix in a single button --- backend/app_sdk/app_base.py | 171 ++++++++++++++++- backend/app_sdk/build.sh | 2 +- backend/go-app/main.go | 245 +++++++++++++++++++++++++ backend/go-app/walkoff.go | 31 ++-- frontend/src/views/AngularWorkflow.jsx | 83 +++++++-- frontend/src/views/Workflows.jsx | 3 + functions/onprem/orborus/Dockerfile | 1 - functions/onprem/orborus/go.mod | 3 +- functions/onprem/orborus/go.sum | 7 + functions/onprem/worker/Dockerfile | 17 +- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 83 ++++++++- 12 files changed, 594 insertions(+), 54 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 1cb3dc80..c369533b 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -9,6 +9,7 @@ import requests import urllib.parse import http.client import urllib3 +import hashlib class AppBase: __version__ = None @@ -91,6 +92,135 @@ class AppBase: else: return {()} + # Handles unique fields by negoiating with the backend + def validate_unique_fields(self, params): + #print("IN THE UNIQUE FIELDS PLACE!") + + newlist = [params] + if isinstance(params, list): + #print("ITS A LIST!") + newlist = params + + #self.full_execution = os.getenv("FULL_EXECUTION", "") + #print(len(params)) + #print(params.items()) + #print(list(params.items())) + #print(f"PARAM: {params}") + #print(f"NEWLIST: {newlist}") + + # FIXME: Also handle MULTI PARAM + values = [] + param_names = [] + all_values = {} + index = 0 + for outerparam in newlist: + + #print(f"INNERTYPE: {type(outerparam)}") + #print(f"HANDLING PARAM {key}") + param_value = "" + for key, value in outerparam.items(): + #print("KEY: %s" % key) + #value = params[key] + for param in self.action["parameters"]: + try: + if param["name"] == key and param["unique_toggled"]: + print(f"FOUND: {key} with param {param}!") + if isinstance(value, dict) or isinstance(value, list): + try: + value = json.dumps(value) + except json.decoder.JSONDecodeError as e: + print(f"Error in json decode for param {value}: {e}") + continue + elif isinstance(value, int) or isinstance(value, float): + value = str(value) + elif value == False: + value = "False" + elif value == True: + value = "True" + + print(f"VALUE APPEND: {value}") + param_value += value + + if param["name"] not in param_names: + param_names.append(param["name"]) + except (KeyError, NameError) as e: + print(f"Key/NameError in param handler: {e}") + + print(f"OUTER VALUE: {param_value}") + if len(param_value) > 0: + md5 = hashlib.md5(param_value.encode('utf-8')).hexdigest() + values.append(md5) + all_values[md5] = { + "index": index, + } + + index += 1 + + # When in here, it means it should be unique + # Should this be done by the backend? E.g. ask it if the value is valid? + # 1. Check if it's unique towards key:value store in org for action + # 2. Check if COMBINATION is unique towards key:value store of action for org + # 3. Have a workflow configuration for unique ID's in unison or per field? E.g. if toggled, then send a hash of all fields together alphabetically, but if not, send one field at a time + + # org_id = full_execution["workflow"]["execution_org"]["id"] + + # USE ARRAY? + + new_params = [] + if len(values) > 0: + org_id = self.full_execution["workflow"]["execution_org"]["id"] + data = { + "append": True, + "workflow_check": False, + "authorization": self.authorization, + "execution_ref": self.current_execution_id, + "org_id": org_id, + "values": [{ + "app": self.action["app_name"], + "action": self.action["name"], + "parameternames": param_names, + "parametervalues": values, + }] + } + + #print(f"DATA: {data}") + # 1594869a676630b397bc34f7dc0951a3 + + #print(f"VALUE URL: {url}") + #print(f"RET: {ret.text}") + #print(f"ID: {ret.status_code}") + url = f"{self.url}/api/v1/orgs/{org_id}/validate_app_values" + ret = requests.post(url, json=data) + if ret.status_code == 200: + json_value = ret.json() + if len(json_value["found"]) > 0: + modifier = 0 + for item in json_value["found"]: + print(f"Should remove {item}") + + try: + print(f"FOUND: {all_values[item]}") + print(f"SHOULD REMOVE INDEX: {all_values[item]['index']}") + + try: + newlist.pop(all_values[item]["index"]-modifier) + modifier += 1 + except IndexError as e: + print(f"Error popping value from array: {e}") + except (NameError, KeyError) as e: + print(f"Failed removal: {e}") + + + #return False + else: + print("None of the items were found!") + return newlist + else: + print(f"[WARNING] Failed checking values with status code {ret.status_code}!") + + #return True + return newlist + # Returns a list of all the executions to be done in the inner loop # FIXME: Doesn't take into account whether you actually WANT to loop or not # Check if the last part of the value is #? @@ -347,6 +477,26 @@ class AppBase: ret = [] param_multiplier = await self.get_param_multipliers(newparams) + # FIXME: This does a deduplication of the data + new_params = self.validate_unique_fields(param_multiplier) + print(f"NEW PARAMS: {new_params}") + if len(new_params) == 0: + print(f"No ID's to handle for validation") + else: + #subparams = new_params + print(f"NEW PARAMS: {new_params}") + + #print("Returned with newparams of length %d", len(new_params)) + #if isinstance(new_params, list) and len(new_params) == 1: + # params = new_params[0] + #else: + # print("[WARNING] SHOULD STOP EXECUTION BECAUSE FIELDS AREN'T UNIQUE") + # action_result["status"] = "SKIPPED" + # action_result["result"] = f"A non-unique value was found" + # action_result["completed_at"] = int(time.time()) + # self.send_result(action_result, headers, stream_path) + # return + print("[INFO] Multiplier length: %d" % len(param_multiplier)) for subparams in param_multiplier: print(f"SUBPARAMS IN MULTI: {subparams}") @@ -1398,6 +1548,7 @@ class AppBase: return True, "" + # THE START IS ACTUALLY RIGHT HERE :O # Checks whether conditions are met, otherwise set branchcheck, tmpresult = check_branch_conditions(action, fullexecution) @@ -1848,14 +1999,19 @@ class AppBase: #print() if not multiexecution: - #newparams.append({ - # "name": val["key"], - # "value": val["value"], - # "variant": "STATIC_VALUE", - # "id": "body_replacement", - #}) + # Runs a single iteration here + new_params = self.validate_unique_fields(params) + print(f"Returned with newparams of length {len(new_params)}") + if isinstance(new_params, list) and len(new_params) == 1: + params = new_params[0] + else: + print("[WARNING] SHOULD STOP EXECUTION BECAUSE FIELDS AREN'T UNIQUE") + action_result["status"] = "SKIPPED" + action_result["result"] = f"A non-unique value was found" + action_result["completed_at"] = int(time.time()) + self.send_result(action_result, headers, stream_path) + return - #print("[INFO] APP_SDK DONE: Starting NORMAL execution of function") print("[INFO] Running normal execution\n") newres = await func(**params) print("\n[INFO] Returned from execution!")#, newres) @@ -2058,7 +2214,6 @@ class AppBase: print("Normal result - no list?") result = results - print("RESULT: %s" % result) action_result["status"] = "SUCCESS" action_result["result"] = str(result) if action_result["result"] == "": diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index ffed66c5..ae25a4a3 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.61 +VERSION=0.8.62 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/main.go b/backend/go-app/main.go index cc620eed..57d57e75 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -7722,6 +7722,247 @@ func handleStopCloudSync(syncUrl string, org Org) error { return nil } +func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Failed reading body"}`)) + return + } + + // Append: Checks if the value should be appended + // WorkflowCheck: Checks if the value should only check for workflow in org, or entire org + // Authorization: the Authorization to use + // ExecutionRef: Ref for the execution + // Values: The values to use + type DataValues struct { + App string + Actions string + ParameterNames []string + ParameterValues []string + } + + type ReturnData struct { + Append bool `json:"append"` + WorkflowCheck bool `json:"workflow_check"` + Authorization string `json:"authorization"` + ExecutionRef string `json:"execution_ref"` + OrgId string `json:"org_id"` + Values []DataValues `json:"values"` + } + + //for key, value := range data.Apps { + var fileId string + location := strings.Split(request.URL.String(), "/") + if location[1] == "api" { + if len(location) <= 4 { + log.Printf("Path too short: %d", len(location)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + var tmpData ReturnData + err = json.Unmarshal(body, &tmpData) + if err != nil { + log.Printf("Failed unmarshalling test: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if tmpData.OrgId != fileId { + log.Printf("[INFO] OrgId %s and %s don't match", tmpData.OrgId, fileId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "Organization ID's don't match"}`)) + return + } + + ctx := context.Background() + + org, err := getOrg(ctx, tmpData.OrgId) + if err != nil { + log.Printf("[INFO] Organization doesn't exist: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowExecution, err := getWorkflowExecution(ctx, tmpData.ExecutionRef) + if err != nil { + log.Printf("[INFO] User can't edit the org") + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "No permission to get execution"}`)) + return + } + + if workflowExecution.Authorization != tmpData.Authorization { + log.Printf("[INFO] Execution auth %s and %s don't match", workflowExecution.Authorization, tmpData.Authorization) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "Auth doesn't match"}`)) + return + } + + if workflowExecution.Status != "EXECUTING" { + log.Printf("[INFO] Workflow isn't executing and shouldn't be searching", workflowExecution.ExecutionId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "Workflow isn't executing"}`)) + return + } + + if workflowExecution.ExecutionOrg != org.Id { + log.Printf("[INFO] Org %s wasn't used to execute %s", org.Id, workflowExecution.ExecutionId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "Bad organization specified"}`)) + return + } + + // Prepared for the future~ + if len(tmpData.Values) != 1 { + log.Printf("[INFO] Filter data can only hande 1 value right now, not %d", len(tmpData.Values)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "Can't handle multiple apps yet, just one"}`)) + return + } + + value := tmpData.Values[0] + + // FIXME: Alphabetically sort the parameternames + // FIXME: Add organization wide search, not just workflow based + + found := []string{} + notFound := []string{} + + dbKey := fmt.Sprintf("app_execution_values") + parameterNames := fmt.Sprintf("%s_%s", value.App, strings.Join(value.ParameterNames, "_")) + log.Printf("[INFO] PARAMNAME: %s", parameterNames) + if tmpData.WorkflowCheck { + //for _, item := range tmpData.Values { + // log.Printf("[INFO] Should validate if values %#v in app parameter %#v exists WITH WORKFLOW %s", item.ParameterValues, item.ParameterNames, workflowExecution.Workflow.ID) + + // FIXME: Make this alphabetical + + for _, value := range value.ParameterValues { + if len(value) == 0 { + log.Printf("Shouldn't have value of length 0!") + continue + } + + log.Printf("[INFO] Looking for value %s", value) + + q := datastore.NewQuery(dbKey).Filter("org_id =", org.Id).Filter("workflow_id =", workflowExecution.Workflow.ID).Filter("parameter_name =", parameterNames).Filter("value =", value) + foundCount, err := dbclient.Count(ctx, q) + if err != nil { + log.Printf("[WARNING] Failed getting key %s: %s", dbKey, err) + notFound = append(notFound, value) + //found = append(found, value) + continue + } + + if foundCount > 0 { + found = append(found, value) + } else { + log.Printf("[INFO] Found for %s: %d", dbKey, foundCount) + notFound = append(notFound, value) + } + } + } else { + log.Printf("[INFO] Should validate if value %s in app %s exists WITH ORG %s", workflowExecution.Workflow.ID) + + for _, value := range value.ParameterValues { + if len(value) == 0 { + log.Printf("Shouldn't have value of length 0!") + continue + } + + log.Printf("[INFO] Looking for value %s", value) + + q := datastore.NewQuery(dbKey).Filter("org_id =", org.Id).Filter("workflow_id =", "").Filter("parameter_name =", parameterNames).Filter("value =", value) + foundCount, err := dbclient.Count(ctx, q) + if err != nil { + log.Printf("[WARNING] Failed getting key %s: %s", dbKey, err) + notFound = append(notFound, value) + //found = append(found, value) + continue + } + + if foundCount > 0 { + found = append(found, value) + } else { + log.Printf("[INFO] Found for %s: %d", dbKey, foundCount) + notFound = append(notFound, value) + } + } + } + + //App string + //Actions string + //ParameterNames string + //ParamererValues []string + + appended := 0 + if tmpData.Append { + log.Printf("[INFO] Should append %d values!", len(notFound)) + dbKey := fmt.Sprintf("app_execution_values") + + //q := datastore.NewQuery(dbKey).Filter("org_id =", org.Id).Filter("workflow_id", workflowExecution.Workflow.ID).Filter("app_name =", parameterNames).Filter("value =", value) + key := datastore.NameKey(dbKey, "", nil) + type NewValue struct { + OrgId string `json:"org_id" datastore:"org_id"` + WorkflowId string `json:"workflow_id" datastore:"workflow_id"` + WorkflowExecutionId string `json:"workflow_execution_id" datastore:"workflow_execution_id"` + ParameterName string `json:"parameter_name" datastore:"parameter_name"` + Value string `json:"value" datastore:"value"` + } + + //parameterNames := strings.Join(value.ParameterNames, "_") + for _, notFoundValue := range notFound { + newRequest := NewValue{ + OrgId: org.Id, + WorkflowExecutionId: workflowExecution.ExecutionId, + ParameterName: parameterNames, + Value: notFoundValue, + } + + if tmpData.WorkflowCheck { + newRequest.WorkflowId = workflowExecution.Workflow.ID + } + + if _, err := dbclient.Put(ctx, key, &newRequest); err != nil { + log.Printf("Error adding %s to appvalue: %s", notFoundValue, err) + continue + } + + appended += 1 + log.Printf("[INFO] Added %s as new appvalue to datastore", notFoundValue) + } + } + + type returnStruct struct { + Success bool `json:"success"` + Appended int `json:"appended"` + Found []string `json:"found"` + } + + returnData := returnStruct{ + Success: true, + Appended: appended, + Found: found, + } + + b, _ := json.Marshal(returnData) + resp.WriteHeader(200) + resp.Write(b) +} + func handleEditOrg(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -8432,6 +8673,10 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/", handleGetOrgs).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", handleGetOrg).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") + + // This is a new API that validates if a key has been seen before. + // Not sure what the best course of action is for it. + r.HandleFunc("/api/v1/orgs/{orgId}/validate_app_values", handleKeyValueCheck).Methods("POST", "OPTIONS") //r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") // Docker orborus specific diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 8f28f72e..05990f8b 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -229,6 +229,7 @@ type WorkflowAppActionParameter struct { Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` SkipMulticheck bool `json:"skip_multicheck" datastore:"skip_multicheck" yaml:"skip_multicheck"` ValueReplace []Valuereplace `json:"value_replace" datastore:"value_replace,noindex" yaml:"value_replace,omitempty"` + UniqueToggled bool `json:"unique_toggled" datastore:"unique_toggled" yaml:"unique_toggled"` } type Valuereplace struct { @@ -1187,6 +1188,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Find underlying nodes and add them } else { log.Printf("[WARNING] Actionresult is %s for node %s in %s. Continuing anyway because of workflow configuration.", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) + // Finds ALL childnodes to set them to SKIPPED childNodes = findChildNodes(*workflowExecution, actionResult.Action.ID) @@ -6119,8 +6121,6 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("[INFO] Starting app hotloading") - cacheKey := fmt.Sprintf("workflowapps-sorted-100") requestCache.Delete(cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") @@ -6149,7 +6149,7 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("[INFO] Hotloading from %s", location) + log.Printf("[INFO] Starting hotloading from %s", location) err = handleAppHotload(location, true) if err != nil { log.Printf("Failed app hotload: %s", err) @@ -6251,6 +6251,20 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) { } else { log.Printf("Updating apps with updates") } + + if tmpBody.ForceUpdate { + ctx := context.Background() + dockercli, err := client.NewEnvClient() + if err == nil { + _, err := dockercli.ImagePull(ctx, "frikky/shuffle:app_sdk", types.ImagePullOptions{}) + if err != nil { + log.Printf("[WARNING] Failed to download apps with the new App SDK: %s", err) + } + } else { + log.Printf("[WARNING] Failed to download apps with the new App SDK because of docker cli: %s", err) + } + } + iterateAppGithubFolders(fs, dir, "", "", tmpBody.ForceUpdate) } else if strings.Contains(tmpBody.URL, "s3") { @@ -6568,17 +6582,6 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin buildLaterList := []buildLaterStruct{} ctx := context.Background() - if forceUpdate { - dockercli, err := client.NewEnvClient() - if err == nil { - _, err := dockercli.ImagePull(ctx, "frikky/shuffle:app_sdk", types.ImagePullOptions{}) - if err != nil { - log.Printf("[WARNING] Failed to download apps with the new App SDK: %s", err) - } - } else { - log.Printf("[WARNING] Failed to download apps with the new App SDK because of docker cli: %s", err) - } - } // It's here to prevent getting them in every iteration for _, file := range dir { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 3485f90c..45f44ba9 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1,5 +1,6 @@ import React, {useState, useEffect, useLayoutEffect} from 'react'; import { useInterval } from 'react-powerhooks'; +import { useTheme } from '@material-ui/core/styles'; import uuid from "uuid"; import {Link} from 'react-router-dom'; @@ -26,6 +27,7 @@ import { w3cwebsocket as W3CWebSocket } from "websocket"; import { useAlert } from "react-alert"; import { validateJson } from "./Workflows.jsx"; import { GetParsedPaths } from "./Apps.jsx"; +import ConfigureWorkflow from '../components/ConfigureWorkflow.jsx'; const surfaceColor = "#27292D" const inputColor = "#383B40" @@ -87,6 +89,7 @@ const AngularWorkflow = (props) => { const referenceUrl = globalUrl+"/api/v1/hooks/" const alert = useAlert() const borderRadius = 3 + const theme = useTheme(); const [bodyWidth, bodyHeight] = useWindowSize(); const appBarSize = 74 @@ -126,6 +129,7 @@ const AngularWorkflow = (props) => { const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false) const [showSkippedActions, setShowSkippedActions] = React.useState(false) const [lastExecution, setLastExecution] = React.useState("") + const [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = React.useState(false) const [curpath, setCurpath] = useState(typeof window === 'undefined' || window.location === undefined ? "" : window.location.pathname) // 0 = normal, 1 = just done, 2 = normal @@ -639,7 +643,7 @@ const AngularWorkflow = (props) => { getWorkflowExecution(props.match.params.key, "") } else if (responseJson.status === "FINISHED") { - console.log("STOPPING BECAUSE ITS OVAH!") + //console.log("STOPPING BECAUSE ITS OVAH!") setExecutionRunning(false) stop() getWorkflowExecution(props.match.params.key, "") @@ -1403,7 +1407,7 @@ const AngularWorkflow = (props) => { // might just be confusing cy.nodes().some(function( ele ) { if (ele.id() !== workflow.start && ele.data()["label"] !== undefined) { - alert.success("Changed startnode to "+ele.data()["label"]) + //alert.success("Changed startnode to "+ele.data()["label"]) ele.data("isStartNode", true) workflow.start = ele.id() //throw BreakException @@ -3734,7 +3738,7 @@ const AngularWorkflow = (props) => { return (
    -
    +
    {data.configuration === true ? @@ -3744,15 +3748,15 @@ const AngularWorkflow = (props) => { }}/> : -
    +
    } -
    +
    {tmpitem}
    - {selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 ? null : + {/*selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 ? null :
    { @@ -3781,6 +3785,29 @@ const AngularWorkflow = (props) => {
    + */} + {selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 && selectedActionParameters[count].required === true && selectedActionParameters[count].unique_toggled !== undefined ? null : +
    + +
    {}}> + { + //console.log("CHECKED!: ", selectedActionParameters[count]) + selectedActionParameters[count].unique_toggled = !selectedActionParameters[count].unique_toggled + selectedAction.parameters[count].unique_toggled = selectedActionParameters[count].unique_toggled + setSelectedActionParameters(selectedActionParameters) + setSelectedAction(selectedAction) + setUpdate(Math.random()) + }} + name="requires_unique" + /> +
    +
    +
    }
    {datafield} @@ -4015,14 +4042,13 @@ const AngularWorkflow = (props) => { /> {selectedApp.name !== undefined && selectedAction.authentication !== undefined && selectedAction.authentication.length === 0 && requiresAuthentication ?
    - Authenticate {selectedApp.name}: - +
    @@ -6117,26 +6143,26 @@ const AngularWorkflow = (props) => { const topBarStyle= { position: "fixed", right: 0, - left: leftBarSize, - top: appBarSize, + left: leftBarSize+20, + top: appBarSize+20, + /* minWidth: cytoscapeViewWidths, maxWidth: cytoscapeViewWidths, - marginLeft: 20, - marginBottom: 20, + */ } const TopCytoscapeBar = () => { return (
    -
    +
    -

    +

    Workflows

    -

    +

    {workflow.name}

    @@ -7472,6 +7498,24 @@ const AngularWorkflow = (props) => { ) } + const configureWorkflowModal = configureWorkflowModalOpen && apps.length !== 0 ? + { + setConfigureWorkflowModalOpen(false) + }} + PaperProps={{ + style: { + backgroundColor: surfaceColor, + color: "white", + minWidth: 600, + padding: 15, + }, + }} + > + + + : null // This whole part is redundant. Made it part of Arguments instead. @@ -7502,6 +7546,7 @@ const AngularWorkflow = (props) => { {conditionsModal} {authenticationModal} {codePopoutModal} + {configureWorkflowModal} { const [editingWorkflow, setEditingWorkflow] = React.useState({}) const [executionLoading, setExecutionLoading] = React.useState(false) const [isDropzone, setIsDropzone] = React.useState(false); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" const { start, stop } = useInterval({ duration: 5000, @@ -1230,11 +1231,13 @@ const Workflows = (props) => { : null} + {isCloud ? null : + } const WorkflowView = () => { diff --git a/functions/onprem/orborus/Dockerfile b/functions/onprem/orborus/Dockerfile index 3fe760c7..7c178fd8 100644 --- a/functions/onprem/orborus/Dockerfile +++ b/functions/onprem/orborus/Dockerfile @@ -2,7 +2,6 @@ FROM golang:1.16.0-buster as builder RUN mkdir /app WORKDIR /app -RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client COPY orborus.go /app/orborus.go RUN go mod init orborus diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 47154cda..4e17a529 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -3,13 +3,14 @@ module orborus go 1.13 require ( + github.com/Microsoft/go-winio v0.4.16 // indirect github.com/containerd/containerd v1.4.3 // indirect github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v20.10.1+incompatible github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/gogo/protobuf v1.3.1 // indirect - github.com/mackerelio/go-osstat v0.1.0 // indirect + github.com/mackerelio/go-osstat v0.1.0 github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index 84c5d0ec..e8c720de 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -41,6 +43,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/mackerelio/go-osstat v0.1.0 h1:e57QHeHob8kKJ5FhcXGdzx5O6Ktuc5RHMDIkeqhgkFA= github.com/mackerelio/go-osstat v0.1.0/go.mod h1:1K3NeYLhMHPvzUu+ePYXtoB58wkaRpxZsGClZBJyIFw= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -53,9 +56,11 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -73,8 +78,10 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190410235845-0ad05ae3009d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/functions/onprem/worker/Dockerfile b/functions/onprem/worker/Dockerfile index 3bf1541a..a9df5c2b 100644 --- a/functions/onprem/worker/Dockerfile +++ b/functions/onprem/worker/Dockerfile @@ -1,18 +1,21 @@ FROM golang:1.16.0-buster as builder WORKDIR /app +RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client +#RUN go env -w GO111MODULE=auto COPY worker.go /app/worker.go -RUN go env -w GO111MODULE=auto - -RUN go get github.com/docker/docker/api/types -RUN go get github.com/docker/docker/api/types/container -RUN go get github.com/docker/docker/client -RUN go get github.com/gorilla/mux -RUN go get github.com/patrickmn/go-cache +RUN go mod init worker +RUN go get github.com/docker/docker/api/types && \ + go get github.com/docker/docker/api/types/container && \ + go get github.com/docker/docker/client && \ + go get github.com/gorilla/mux && \ + go get github.com/patrickmn/go-cache +RUN go build RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker . +## ALPINE IMAGE FROM alpine:3.12 ENV SHUFFLE_BASE_IMAGE_REGISTRY=docker.io diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 3f2a483e..44d134c5 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-worker -VERSION=0.8.62 +VERSION=0.8.63 echo "Running docker build with $NAME:$VERSION" #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 3e15c282..0594b142 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -524,7 +524,7 @@ type WorkflowAppActionParameter struct { Description string `json:"description" datastore:"description,noindex" yaml:"description"` ID string `json:"id" datastore:"id" yaml:"id,omitempty"` Name string `json:"name" datastore:"name" yaml:"name"` - Example string `json:"example" datastore:"example" yaml:"example"` + Example string `json:"example" datastore:"example,noindex" yaml:"example"` Value string `json:"value" datastore:"value,noindex" yaml:"value,omitempty"` Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` Options []string `json:"options" datastore:"options" yaml:"options"` @@ -536,6 +536,7 @@ type WorkflowAppActionParameter struct { Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` SkipMulticheck bool `json:"skip_multicheck" datastore:"skip_multicheck" yaml:"skip_multicheck"` ValueReplace []Valuereplace `json:"value_replace" datastore:"value_replace,noindex" yaml:"value_replace,omitempty"` + UniqueToggled bool `json:"unique_toggled" datastore:"unique_toggled" yaml:"unique_toggled"` } type Valuereplace struct { @@ -2335,9 +2336,9 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } else { log.Printf("[WARNING] Actionresult is %s for node %s in %s. Continuing anyway because of workflow configuration.", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) // Finds ALL childnodes to set them to SKIPPED - childNodes = findChildNodes(*workflowExecution, actionResult.Action.ID) // Remove duplicates //log.Printf("CHILD NODES: %d", len(childNodes)) + childNodes = findChildNodes(*workflowExecution, actionResult.Action.ID) for _, nodeId := range childNodes { if nodeId == actionResult.Action.ID { continue @@ -2490,6 +2491,84 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl log.Printf("[INFO] Setting value (2) of %s in execution %s to %s. New result length: %d", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status, len(workflowExecution.Results)) } + if actionResult.Status == "SKIPPED" { + log.Printf("\n\n[INFO] Handling special case for SKIPPED!\n\n") + childNodes := findChildNodes(*workflowExecution, actionResult.Action.ID) + for _, nodeId := range childNodes { + if nodeId == actionResult.Action.ID { + continue + } + + // 1. Find the action itself + // 2. Create an actionresult + curAction := Action{ID: ""} + for _, action := range workflowExecution.Workflow.Actions { + if action.ID == nodeId { + curAction = action + break + } + } + + if len(curAction.ID) == 0 { + log.Printf("Couldn't find subnode %s", nodeId) + continue + } + + resultExists := false + for _, result := range workflowExecution.Results { + if result.Action.ID == curAction.ID { + resultExists = true + break + } + } + + if !resultExists { + // Check parents are done here. Only add it IF all parents are skipped + skipNodeAdd := false + for _, branch := range workflowExecution.Workflow.Branches { + if branch.DestinationID == nodeId { + // If the branch's source node is NOT in childNodes, it's not a skipped parent + sourceNodeFound := false + for _, item := range childNodes { + if item == branch.SourceID { + sourceNodeFound = true + break + } + } + + if !sourceNodeFound { + log.Printf("[INFO] Not setting node %s to SKIPPED", nodeId) + skipNodeAdd = true + break + } + } + } + + if !skipNodeAdd { + newAction := Action{ + AppName: curAction.AppName, + AppVersion: curAction.AppVersion, + Label: curAction.Label, + Name: curAction.Name, + ID: curAction.ID, + } + + newResult := ActionResult{ + Action: newAction, + ExecutionId: actionResult.ExecutionId, + Authorization: actionResult.Authorization, + Result: "Skipped because of previous node", + StartedAt: 0, + CompletedAt: 0, + Status: "SKIPPED", + } + + workflowExecution.Results = append(workflowExecution.Results, newResult) + } + } + } + } + // FIXME: Have a check for skippednodes and their parents /* for resultIndex, result := range workflowExecution.Results { From b6dcc01e8f511f1c10e85d892db2656d5dadc407 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 11 Mar 2021 17:15:05 +0100 Subject: [PATCH 161/185] #293: Added extra configuration to 0.8.63 --- backend/app_sdk/build.sh | 2 +- docker-compose.yml | 4 ++-- functions/onprem/orborus/build.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index ae25a4a3..258a65db 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.62 +VERSION=0.8.63 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/docker-compose.yml b/docker-compose.yml index ff2bf5d4..c8d2d2ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.62 + image: ghcr.io/frikky/shuffle-orborus:0.8.63 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -56,7 +56,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - SHUFFLE_APP_SDK_VERSION=0.8.60 - - SHUFFLE_WORKER_VERSION=0.8.62 + - SHUFFLE_WORKER_VERSION=0.8.63 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 4c0b9450..1555b51f 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.62 +VERSION=0.8.63 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force From 33ac163ef64ac93de73b928346034438d9091d80 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Mar 2021 06:47:04 +0100 Subject: [PATCH 162/185] Made saving a workflow fast again --- backend/app_sdk/app_base.py | 12 +++++- backend/go-app/codegen.go | 33 +++++++++++++-- backend/go-app/walkoff.go | 31 +++++++------- docker-compose.yml | 4 +- frontend/src/components/AlertTemplate.js | 1 + frontend/src/views/Admin.jsx | 2 +- frontend/src/views/AppCreator.jsx | 12 +++--- frontend/src/views/Apps.jsx | 53 +++++++++++++++++------- 8 files changed, 104 insertions(+), 44 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index c369533b..c2667fec 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -481,10 +481,17 @@ class AppBase: new_params = self.validate_unique_fields(param_multiplier) print(f"NEW PARAMS: {new_params}") if len(new_params) == 0: - print(f"No ID's to handle for validation") + print("[WARNING] SHOULD STOP MULTI-EXECUTION BECAUSE FIELDS AREN'T UNIQUE") + action_result["status"] = "SKIPPED" + action_result["result"] = f"All values were non-unique" + action_result["completed_at"] = int(time.time()) + self.send_result(action_result, headers, stream_path) + exit() + #return else: #subparams = new_params print(f"NEW PARAMS: {new_params}") + param_multiplier = new_params #print("Returned with newparams of length %d", len(new_params)) #if isinstance(new_params, list) and len(new_params) == 1: @@ -1821,14 +1828,15 @@ class AppBase: if len(itemlist) > curminlength: curminlength = len(itemlist) + except json.decoder.JSONDecodeError as e: print("JSON Error: %s in %s" % (e, actualitem)) replacements[to_be_replaced] = actualitem + #print("In second part of else: %s" % (len(itemlist))) # This is a result array for JUST this value.. # What if there are more? - print("LENGTH: %d. In second part of else: %s" % (len(itemlist), replacements)) resultarray = [] for i in range(0, curminlength): tmpitem = json.loads(json.dumps(parameter["value"])) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 4389219f..b0b9b14d 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -1105,13 +1105,16 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ } } - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ Name: "ssl_verify", Description: "Check if you want to verify request", Multiline: false, Required: false, Example: "True", + Options: []string{ + "True", + "False", + }, Schema: SchemaDefinition{ Type: "string", }, @@ -1244,10 +1247,14 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ Name: "ssl_verify", - Description: "Check if you want to verify the SSL certificate request", + Description: "Check if you want to verify request", Multiline: false, Required: false, - Example: "False - default=True", + Example: "True", + Options: []string{ + "True", + "False", + }, Schema: SchemaDefinition{ Type: "string", }, @@ -1382,6 +1389,10 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo Multiline: false, Required: false, Example: "True", + Options: []string{ + "True", + "False", + }, Schema: SchemaDefinition{ Type: "string", }, @@ -1517,6 +1528,10 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] Multiline: false, Required: false, Example: "True", + Options: []string{ + "True", + "False", + }, Schema: SchemaDefinition{ Type: "string", }, @@ -1684,6 +1699,10 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo Multiline: false, Required: false, Example: "True", + Options: []string{ + "True", + "False", + }, Schema: SchemaDefinition{ Type: "string", }, @@ -1823,6 +1842,10 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W Multiline: false, Required: false, Example: "True", + Options: []string{ + "True", + "False", + }, Schema: SchemaDefinition{ Type: "string", }, @@ -1958,6 +1981,10 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor Multiline: false, Required: false, Example: "True", + Options: []string{ + "True", + "False", + }, Schema: SchemaDefinition{ Type: "string", }, diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 05990f8b..3d9923b5 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2224,6 +2224,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } + log.Printf("PRE BODY") body, err := ioutil.ReadAll(request.Body) if err != nil { log.Printf("Failed hook unmarshaling: %s", err) @@ -2267,6 +2268,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { allNodes := []string{} workflow.Categories = Categories{} + log.Printf("PRE APPS") workflowapps, apperr := getAllWorkflowApps(ctx, 500) //log.Printf("Action: %#v", action.Authentication) @@ -2299,6 +2301,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { newActions = append(newActions, action) } + log.Printf("PRE SAVECHECK") if !workflow.PreviouslySaved { log.Printf("[WORKFLOW INIT] NOT PREVIOUSLY SAVED - SET ACTION AUTH!") //AuthenticationId string `json:"authentication_id,omitempty" datastore:"authentication_id"` @@ -2434,6 +2437,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.PreviouslySaved = true } + log.Printf("PRE TRIGGERS") workflow.Actions = newActions newTriggers := []Trigger{} for _, trigger := range workflow.Triggers { @@ -2549,6 +2553,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Triggers = newTriggers + log.Printf("PRE VARIABLES") for _, variable := range workflow.WorkflowVariables { if len(variable.Value) == 0 { log.Printf("Can't have an empty variable: %s", variable.Name) @@ -2596,6 +2601,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // FIXME - append all nodes (actions, triggers etc) to one single array here + log.Printf("PRE VARIABLES") if len(foundNodes) != len(allNodes) || len(workflow.Actions) <= 0 { // This shit takes a few seconds lol if !workflow.IsValid { @@ -2637,18 +2643,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // Have to do it like this to add the user's apps //log.Println("Apps set starting") //log.Printf("EXIT ON ERROR: %#v", workflow.Configuration.ExitOnError) - workflowApps := []WorkflowApp{} - //memcacheName = "all_apps" - //if item, err := memcache.Get(ctx, memcacheName); err == memcache.ErrCacheMiss { - // // Not in cache - // log.Printf("Apps not in cache.") - workflowApps, err = getAllWorkflowApps(ctx, 100) - if err != nil { - log.Printf("Failed getting all workflow apps from database: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } + //workflowapps, apperr := getAllWorkflowApps(ctx, 500) // Started getting the single apps, but if it's weird, this is faster // 1. Check workflow.Start @@ -2680,6 +2675,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // Check every app action and param to see whether they exist + log.Printf("PRE ACTIONS 2") newActions = []Action{} for _, action := range workflow.Actions { reservedApps := []string{ @@ -2731,7 +2727,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } else { curapp := WorkflowApp{} // FIXME - can this work with ONLY AppID? - for _, app := range workflowApps { + for _, app := range workflowapps { if app.ID == action.AppID { curapp = app break @@ -2860,10 +2856,11 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { Errors: workflow.Errors, } - cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) - cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + // Really don't know why this was happening + //cacheKey := fmt.Sprintf("workflowapps-sorted-100") + //requestCache.Delete(cacheKey) + //cacheKey = fmt.Sprintf("workflowapps-sorted-500") + //requestCache.Delete(cacheKey) log.Printf("[INFO] Saved new version of workflow %s (%s) for org %s", workflow.Name, fileId, workflow.OrgId) resp.WriteHeader(200) diff --git a/docker-compose.yml b/docker-compose.yml index c8d2d2ab..b26f3c1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.62 + image: ghcr.io/frikky/shuffle-frontend:0.8.63 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.62 + image: ghcr.io/frikky/shuffle-backend:0.8.63 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: diff --git a/frontend/src/components/AlertTemplate.js b/frontend/src/components/AlertTemplate.js index a476afc7..58ff46cc 100644 --- a/frontend/src/components/AlertTemplate.js +++ b/frontend/src/components/AlertTemplate.js @@ -18,6 +18,7 @@ const alertStyle = { width: 400, boxSizing: 'border-box', zIndex: 100001, + overflow: "hidden", } const buttonStyle = { diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 7cee4bbe..c9f85d55 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1958,7 +1958,7 @@ const Admin = (props) => { primary={new Date(file.created_at*1000).toISOString()} /> { } } - console.log(methodvalue["requestBody"]["content"]) + //console.log(methodvalue["requestBody"]["content"]) if (methodvalue["requestBody"]["content"]["multipart/form-data"] !== undefined) { if (methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"] !== undefined && methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"] !== null) { if (methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["type"] === "object") { @@ -1796,8 +1796,8 @@ const AppCreator = (props) => { ) })} - {actionBodyRequest.map(data => ( - + {actionBodyRequest.map((data, index) => ( + {data} ))} @@ -1833,8 +1833,8 @@ const AppCreator = (props) => { console.log("URL: ", parsedurl) if (parsedurl.includes("<") && parsedurl.includes(">")) { console.log("REPLACE") - parsedurl = parsedurl.replace("<", "{") - parsedurl = parsedurl.replace(">", "}") + parsedurl = parsedurl.replaceAll("<", "{") + parsedurl = parsedurl.replaceAll(">", "}") } if (parsedurl.startsWith("PUT ") || parsedurl.startsWith("GET ") ||parsedurl.startsWith("POST ") || parsedurl.startsWith("DELETE ") ||parsedurl.startsWith("PATCH ") || parsedurl.startsWith("CONNECT ")) { @@ -2208,8 +2208,10 @@ const AppCreator = (props) => { }}/> const zoomIn = () => { + console.log("ZOOOMING IN") setScale(scale+0.1); } + const zoomOut = () => { setScale(scale-0.1); } diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 04fb0a62..0b4cd611 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -202,14 +202,40 @@ const Apps = (props) => { .then((responseJson) => { //console.log("Apps: ", responseJson) //responseJson = sortByKey(responseJson, "large_image") - responseJson = sortByKey(responseJson, "generated") + //responseJson = sortByKey(responseJson, "is_valid") + //setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated))) + + var privateapps = [] + var valid = [] + var invalid = [] + for (var key in responseJson) { + const app = responseJson[key] + if (app.is_valid && !(!app.activated && app.generated)) { + privateapps.push(app) + } else if (app.private_id !== undefined && app.private_id.length > 0) { + valid.push(app) + } else { + invalid.push(app) + } + } - setApps(responseJson) - setFilteredApps(responseJson) - if (responseJson.length > 0) { - setSelectedApp(responseJson[0]) - if (responseJson[0].actions !== null && responseJson[0].actions.length > 0) { - setSelectedAction(responseJson[0].actions[0]) + //console.log(privateapps) + //console.log(valid) + //console.log(invalid) + //console.log(privateapps) + //privateapps.reverse() + privateapps.push(...valid) + privateapps.push(...invalid) + + setApps(privateapps) + setFilteredApps(privateapps) + if (privateapps.length > 0) { + if (selectedApp.id === undefined || selectedApp.id === null) { + setSelectedApp(privateapps[0]) + } + + if (privateapps[0].actions !== null && privateapps[0].actions.length > 0) { + setSelectedAction(privateapps[0].actions[0]) } else { setSelectedAction({}) } @@ -358,8 +384,7 @@ const Apps = (props) => { {imageline} -
    -
    +
    @@ -957,7 +982,7 @@ const Apps = (props) => { setValidation(true) setIsLoading(true) - start() + //start() const parsedData = { "url": url, @@ -989,10 +1014,10 @@ const Apps = (props) => { if (response.status === 200) { alert.success("Loaded existing apps!") } - setIsLoading(false) - stop() - setValidation(false) + //stop() + setIsLoading(false) + setValidation(false) return response.json() }) .then((responseJson) => { @@ -1005,7 +1030,7 @@ const Apps = (props) => { console.log("ERROR: ", error.toString()) alert.error(error.toString()) - stop() + //stop() setIsLoading(false) setValidation(false) }) From 49d9c63d88d42462780615516caa97cb014b2a92 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Mar 2021 07:36:08 +0100 Subject: [PATCH 163/185] Fixed loop-match regex for apps --- backend/app_sdk/app_base.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index c2667fec..b3cd2f7e 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1299,6 +1299,7 @@ class AppBase: # Matches with space in the first part, but not in subsequent parts. # JSON / yaml etc shouldn't have spaces in their fields anyway. + #match = ".*?([$]{1}([a-zA-Z0-9 _-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})[$/, ]?" match = ".*?([$]{1}([a-zA-Z0-9 _-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})" # Regex to find all the things @@ -1699,7 +1700,9 @@ class AppBase: # Custom format for ${name[0,1,2,...]}$ #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" - submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" + print(f"Returnedvalue: {value}") + #submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" + submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*?]}\$))" actualitem = re.findall(submatch, value, re.MULTILINE) try: if action["skip_multicheck"]: @@ -1806,7 +1809,7 @@ class AppBase: multi_execution_lists.append(new_replacement) #print("MULTI finished: %s" % json_replacement) else: - print("(2) Pre replacement (loop with variables). ") #% actualitem) + print(f"(2) Pre replacement (loop with variables). Variables: {actualitem}") #% actualitem) # This is here to handle for loops within variables.. kindof # 1. Find the length of the longest array # 2. Build an array with the base values based on parameter["value"] @@ -1830,7 +1833,7 @@ class AppBase: curminlength = len(itemlist) except json.decoder.JSONDecodeError as e: - print("JSON Error: %s in %s" % (e, actualitem)) + print("JSON Error (replace): %s in %s" % (e, actualitem)) replacements[to_be_replaced] = actualitem From 4305b8275d9b42cb651869948dd9588908736d16 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Mar 2021 18:39:33 +0100 Subject: [PATCH 164/185] Started re-adding oauth2 configurations --- backend/app_sdk/app_base.py | 98 ++++- backend/go-app/main.go | 558 +------------------------ backend/go-app/oauth2.go | 468 +++++++++++++++++++++ backend/go-app/walkoff.go | 16 +- frontend/src/views/AngularWorkflow.jsx | 114 ++--- 5 files changed, 633 insertions(+), 621 deletions(-) create mode 100644 backend/go-app/oauth2.go diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index b3cd2f7e..b9eee023 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -28,6 +28,7 @@ class AppBase: self.authorization = os.getenv("AUTHORIZATION", "") self.current_execution_id = os.getenv("EXECUTIONID", "") self.full_execution = os.getenv("FULL_EXECUTION", "") + self.start_time = int(time.time()) self.result_wrapper_count = 0 if isinstance(self.action, str): @@ -482,10 +483,17 @@ class AppBase: print(f"NEW PARAMS: {new_params}") if len(new_params) == 0: print("[WARNING] SHOULD STOP MULTI-EXECUTION BECAUSE FIELDS AREN'T UNIQUE") - action_result["status"] = "SKIPPED" - action_result["result"] = f"All values were non-unique" - action_result["completed_at"] = int(time.time()) - self.send_result(action_result, headers, stream_path) + action_result = { + "action": self.action, + "authorization": self.authorization, + "execution_id": self.current_execution_id, + "result": f"All {len(param_multiplier)} values were non-unique", + "started_at": self.start_time, + "status": "SKIPPED", + "completed_at": int(time.time()), + } + + self.send_result(action_result, {"Content-Type": "application/json", "Authorization": "Bearer %s" % self.authorization}, "/api/v1/streams") exit() #return else: @@ -906,6 +914,41 @@ class AppBase: return data.strip() if "split" in thistype: return data.split() + if "join" in thistype: + print(f"SHOULD JOIN: {data}") + try: + splitvalues = data.split(",") + if "," not in data: + return f"join({data})" + + if len(splitvalues) >= 2: + print(f"SPLITVALUE: {splitvalues[-1]}") + + # 1. Take the list and parse it from string + # 2. Take all the items and join them + # 3. Parse them back as string and return + values = ",".join(splitvalues[0:-1]) + print(f"VALUES: {values}") + tmp = json.loads(values) + print(f"TMP: {tmp}") + #tmp = tmp[1:-1] + #print(f"TMP2: {tmp}") + try: + newvalues = splitvalues[-1].join(str(item).strip() for item in tmp) + except TypeError: + newvalues = splitvalues[-1].join(json.dumps(item).strip() for item in tmp) + + print(f"new: {newvalues}") + return newvalues + else: + print("Returning default") + return f"join({data})" + + except (KeyError, IndexError) as e: + print(f"ERROR in join(): {e}") + except json.decoder.JSONDecodeError as e: + print(f"JSON ERROR in join(): {e}") + if "len" in thistype or "length" in thistype or "lenght" in thistype: tmp = "" try: @@ -919,9 +962,9 @@ class AppBase: pass if isinstance(tmp, list): - return len(tmp) + return str(len(tmp)) elif isinstance(tmp, object): - return len(tmp) + return str(len(tmp)) return str(len(data)) if "parse" in thistype: @@ -967,7 +1010,7 @@ class AppBase: #print("Running %s" % data) # Look for the INNER wrapper first, then move out - wrappers = ["int", "number", "lower", "upper", "trim", "strip", "split", "parse", "len", "length", "lenght"] + wrappers = ["int", "number", "lower", "upper", "trim", "strip", "split", "parse", "len", "length", "lenght", "join"] found = False for wrapper in wrappers: if wrapper not in data.lower(): @@ -982,8 +1025,8 @@ class AppBase: # Do stuff here. innervalue = parse_nested_param(data, maxDepth(data)-0) outervalue = parse_nested_param(data, maxDepth(data)-1) - #print("INNER: ", innervalue) - #print("OUTER: ", outervalue) + print("INNER: ", innervalue) + print("OUTER: ", outervalue) if outervalue != innervalue: #print("Outer: ", outervalue, " inner: ", innervalue) @@ -1224,10 +1267,10 @@ class AppBase: baseresult = variable["value"] break except KeyError as e: - print("KeyError wf variables: %s" % e) + print("[INFO] KeyError wf variables: %s" % e) pass except TypeError as e: - print("TypeError wf variables: %s" % e) + print("[INFO] TypeError wf variables: %s" % e) pass print("BEFORE EXECUTION VAR") @@ -1240,10 +1283,10 @@ class AppBase: baseresult = variable["value"] break except KeyError as e: - print("KeyError exec variables: %s" % e) + print("[INFO] KeyError exec variables: %s" % e) pass except TypeError as e: - print("TypeError exec variables: %s" % e) + print("[INFO] TypeError exec variables: %s" % e) pass except KeyError as error: @@ -1701,7 +1744,8 @@ class AppBase: # Custom format for ${name[0,1,2,...]}$ #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" print(f"Returnedvalue: {value}") - #submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" + # OLD: Used until 13.03.2021: submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" + # \${[0-9a-zA-Z_-]+#?(\[.*?]}\$) submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*?]}\$))" actualitem = re.findall(submatch, value, re.MULTILINE) try: @@ -1722,18 +1766,24 @@ class AppBase: # Loop WITH variables go in else. print("Before first part in multiexec!") handled = False + + # Has a loop without a variable used inside if len(actualitem[0]) > 2 and actualitem[0][1] == "SHUFFLE_NO_SPLITTER": print("(1) Pre replacement: %s" % actualitem[0][2]) tmpitem = value - replacement = actualitem[0][2] + index = 0 + replacement = actualitem[index][2] + if replacement.endswith("}$"): + replacement = replacement[:-2] + if replacement.startswith("\"") and replacement.endswith("\""): replacement = replacement[1:len(replacement)-1] print("POST replacement: %s" % replacement) - #json_replacement = tmpitem.replace(actualitem[0][0], replacement, 1) + #json_replacement = tmpitem.replace(actualitem[index][0], replacement, 1) #print("AFTER POST replacement: %s" % json_replacement) #json_replacement = replacement try: @@ -1755,9 +1805,9 @@ class AppBase: for i in range(len(json_replacement)): if isinstance(json_replacement[i], dict) or isinstance(json_replacement[i], list): tmp_replacer = json.dumps(json_replacement[i]) - newvalue = tmpitem.replace(actualitem[0][0], tmp_replacer, 1) + newvalue = tmpitem.replace(actualitem[index][0], tmp_replacer, 1) else: - newvalue = tmpitem.replace(actualitem[0][0], json_replacement[i], 1) + newvalue = tmpitem.replace(actualitem[index][0], json_replacement[i], 1) try: newvalue = json.loads(newvalue) @@ -1770,7 +1820,7 @@ class AppBase: print("New replacement: %s" % new_replacement) # New - tmpitem = tmpitem.replace(actualitem[0][0], replacement, 1) + tmpitem = tmpitem.replace(actualitem[index][0], replacement, 1) # This code handles files. resultarray = [] @@ -1821,9 +1871,15 @@ class AppBase: try: to_be_replaced = replace[0] actualitem = replace[2] + if actualitem.endswith("}$"): + actualitem = actualitem[:-2] except IndexError: continue + #print(f"\n\nTMPITEM: {actualitem}\n\n") + #actualitem = parse_wrapper_start(actualitem) + #print(f"\n\nTMPITEM2: {actualitem}\n\n") + try: itemlist = json.loads(actualitem) if len(itemlist) > minlength: @@ -1837,6 +1893,10 @@ class AppBase: replacements[to_be_replaced] = actualitem + + # Parses the data as string with length, split etc. before moving on. + + #print("In second part of else: %s" % (len(itemlist))) # This is a result array for JUST this value.. # What if there are more? diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 57d57e75..7dc85deb 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -42,7 +42,6 @@ import ( */ "github.com/google/go-github/v28/github" - "golang.org/x/oauth2" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" @@ -671,7 +670,7 @@ func handleApiAuthentication(resp http.ResponseWriter, request *http.Request) (U // Should basically never happen Userdata, err := getUser(ctx, session.Id) if err != nil { - log.Printf("Username %s doesn't exist (authcheck): %s", session.Username, err) + log.Printf("[INFO] Username %s doesn't exist (authcheck): %s", session.Username, err) return User{}, err } @@ -1770,90 +1769,6 @@ type passwordChange struct { Currentpassword string `json:"currentpassword"` } -func handlePasswordResetMail(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - log.Println("Handling password reset mail") - defaultMessage := "We have sent you an email :)" - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Println("Failed reading body") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, defaultMessage))) - return - } - - type passwordReset struct { - Username string `json:"username"` - } - - var t passwordReset - err = json.Unmarshal(body, &t) - if err != nil { - log.Printf("Failed unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, defaultMessage))) - return - } - - ctx := context.Background() - Userdata, err := getUser(ctx, t.Username) - if err != nil { - log.Printf("Username %s doesn't exist (pw reset mail): %s", t.Username, err) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) - return - } - - resetToken := uuid.NewV4() - // FIXME: - // Weakness with this system is that you can spam someone with password resets, - // and they would never be able to reset, as a new token is always generated - url := fmt.Sprintf("https://shuffler.io/passwordreset/%s", resetToken.String()) - - Userdata.ResetReference = resetToken.String() - Userdata.ResetTimeout = 0 - err = setUser(ctx, Userdata) - if err != nil { - log.Printf("Error patching User for mail %s: %s", Userdata.Username, err) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) - return - } - - log.Printf("%#v", Userdata) - addr := t.Username - const confirmMessage = ` -Reset URL :) - -%s - ` - - msg := &mail.Message{ - Sender: "Shuffle ", - To: []string{addr}, - Subject: "Reset your password - Shuffle", - Body: fmt.Sprintf(confirmMessage, url), - } - - log.Println(msg.Body) - if err := mail.Send(ctx, msg); err != nil { - log.Printf("Couldn't send email: %v", err) - } - - // FIXME - // Generate an email to send - // Generate a reset code with a reset link - // Build frontend to handle reset link with "new password" etc. - - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "%s"}`, defaultMessage))) -} - func handlePasswordReset(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -4954,398 +4869,6 @@ func getDocs(resp http.ResponseWriter, request *http.Request) { resp.Write(b) } -type OutlookProfile struct { - OdataContext string `json:"@odata.context"` - BusinessPhones []string `json:"businessPhones"` - DisplayName string `json:"displayName"` - GivenName string `json:"givenName"` - JobTitle interface{} `json:"jobTitle"` - Mail string `json:"mail"` - MobilePhone interface{} `json:"mobilePhone"` - OfficeLocation interface{} `json:"officeLocation"` - PreferredLanguage interface{} `json:"preferredLanguage"` - Surname string `json:"surname"` - UserPrincipalName string `json:"userPrincipalName"` - ID string `json:"id"` -} - -type OutlookFolder struct { - ID string `json:"id"` - DisplayName string `json:"displayName"` - ParentFolderID string `json:"parentFolderId"` - ChildFolderCount int `json:"childFolderCount"` - UnreadItemCount int `json:"unreadItemCount"` - TotalItemCount int `json:"totalItemCount"` -} - -type OutlookFolders struct { - OdataContext string `json:"@odata.context"` - OdataNextLink string `json:"@odata.nextLink"` - Value []OutlookFolder `json:"value"` -} - -func getOutlookFolders(client *http.Client) (OutlookFolders, error) { - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/frikky@shuffletest.onmicrosoft.com/mailfolders") - - ret, err := client.Get(requestUrl) - if err != nil { - log.Printf("FolderErr: %s", err) - return OutlookFolders{}, err - } - - if ret.StatusCode != 200 { - log.Printf("Status folders: %d", ret.StatusCode) - return OutlookFolders{}, err - } - - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - log.Printf("Body: %s", err) - return OutlookFolders{}, err - } - - //log.Printf("Body: %s", string(body)) - - mailfolders := OutlookFolders{} - err = json.Unmarshal(body, &mailfolders) - if err != nil { - log.Printf("Unmarshal: %s", err) - return OutlookFolders{}, err - } - - //fmt.Printf("%#v", mailfolders) - // FIXME - recursion for subfolders - // Recursive struct - // folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId) - //for _, folder := range mailfolders.Value { - // log.Println(folder.DisplayName) - //} - - return mailfolders, nil -} - -func getOutlookProfile(client *http.Client) (OutlookProfile, error) { - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me?$select=mail") - - ret, err := client.Get(requestUrl) - if err != nil { - log.Printf("FolderErr: %s", err) - return OutlookProfile{}, err - } - - log.Printf("Status folders: %d", ret.StatusCode) - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - log.Printf("Body: %s", err) - return OutlookProfile{}, err - } - - profile := OutlookProfile{} - err = json.Unmarshal(body, &profile) - if err != nil { - log.Printf("Unmarshal: %s", err) - return OutlookProfile{}, err - } - - return profile, nil -} - -func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { - code := request.URL.Query().Get("code") - if len(code) == 0 { - log.Println("No code") - resp.WriteHeader(401) - return - } - - url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) - log.Println(url) - ctx := context.Background() - client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) - if err != nil { - log.Printf("Oauth client failure - outlook register: %s", err) - resp.WriteHeader(401) - return - } - // This should be possible, and will also give the actual username - profile, err := getOutlookProfile(client) - if err != nil { - log.Printf("Outlook profile failure: %s", err) - resp.WriteHeader(401) - return - } - - // This is a state workaround, which should really be for CSRF checks lol - state := request.URL.Query().Get("state") - if len(state) == 0 { - log.Println("No state") - resp.WriteHeader(401) - return - } - - stateitems := strings.Split(state, "%26") - if len(stateitems) == 1 { - stateitems = strings.Split(state, "&") - } - - // FIXME - trigger auth - senderUser := "" - trigger := TriggerAuth{} - for _, item := range stateitems { - itemsplit := strings.Split(item, "%3D") - if len(itemsplit) == 1 { - itemsplit = strings.Split(item, "=") - } - - if len(itemsplit) != 2 { - continue - } - - // Do something here - if itemsplit[0] == "workflow_id" { - trigger.WorkflowId = itemsplit[1] - } else if itemsplit[0] == "trigger_id" { - trigger.Id = itemsplit[1] - } else if itemsplit[0] == "type" { - trigger.Type = itemsplit[1] - } else if itemsplit[0] == "username" { - trigger.Username = itemsplit[1] - trigger.Owner = itemsplit[1] - senderUser = itemsplit[1] - } - } - - // THis is an override based on the user in oauth return - trigger.Username = profile.Mail - trigger.Code = code - trigger.OauthToken = OauthToken{ - AccessToken: accessToken.AccessToken, - TokenType: accessToken.TokenType, - RefreshToken: accessToken.RefreshToken, - Expiry: accessToken.Expiry, - } - - //log.Printf("%#v", trigger) - if trigger.WorkflowId == "" || trigger.Id == "" || trigger.Username == "" || trigger.Type == "" { - log.Printf("All oauth items need to contain data to register a new state") - resp.WriteHeader(401) - return - } - - // Should also update the user - Userdata, err := getUser(ctx, senderUser) - if err != nil { - log.Printf("Username %s doesn't exist (oauth2): %s", trigger.Username, err) - resp.WriteHeader(401) - return - } - - Userdata.Authentication = append(Userdata.Authentication, UserAuth{ - Name: "Outlook", - Description: "oauth2", - Workflows: []string{trigger.WorkflowId}, - Username: trigger.Username, - Fields: []UserAuthField{ - UserAuthField{ - Key: "trigger_id", - Value: trigger.Id, - }, - UserAuthField{ - Key: "username", - Value: trigger.Username, - }, - UserAuthField{ - Key: "code", - Value: code, - }, - UserAuthField{ - Key: "type", - Value: trigger.Type, - }, - }, - }) - - // Set apikey for the user if they don't have one - if len(Userdata.ApiKey) == 0 { - newUser, err := generateApikey(ctx, *Userdata) - Userdata = &newUser - if err != nil { - log.Printf("Failed to generate apikey for user %s when creating outlook sub: %s", Userdata.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - } - - //err = setUser(Userdata) - //if err != nil { - // log.Printf("Failed setting user data for %s: %s", Userdata.Username, err) - // resp.WriteHeader(401) - // return - //} - - err = setTriggerAuth(ctx, trigger) - if err != nil { - log.Printf("Failed to set trigger auth for %s - %s", trigger.Username, err) - resp.WriteHeader(401) - return - } - - // FIXME - not sure if these are good at all :) - environmentVariables := map[string]string{ - "FUNCTION_APIKEY": Userdata.ApiKey, - "CALLBACKURL": "https://shuffler.io", - "WORKFLOW_ID": trigger.WorkflowId, - "TRIGGER_ID": trigger.Id, - } - - applocation := fmt.Sprintf("gs://%s/triggers/outlooktrigger.zip", bucketName) - hookname := fmt.Sprintf("outlooktrigger_%s", trigger.Id) - - err = deployCloudFunctionGo(ctx, hookname, defaultLocation, applocation, environmentVariables) - if err != nil { - log.Printf("Error deploying hook: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Issue with starting hook. Please wait a second and try again"}`))) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -type OauthToken struct { - AccessToken string `json:"AccessToken" datastore:"AccessToken,noindex"` - TokenType string `json:"TokenType" datastore:"TokenType,noindex"` - RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"` - Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"` -} -type TriggerAuth struct { - Id string `json:"id" datastore:"id"` - SubscriptionId string `json:"subscriptionId" datastore:"subscriptionId"` - - Username string `json:"username" datastore:"username,noindex"` - WorkflowId string `json:"workflow_id" datastore:"workflow_id,noindex"` - Owner string `json:"owner" datastore:"owner"` - Type string `json:"type" datastore:"type"` - Code string `json:"code,omitempty" datastore:"code,noindex"` - OauthToken OauthToken `json:"oauth_token,omitempty" datastore:"oauth_token"` -} - -func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { - key := datastore.NameKey("trigger_auth", strings.ToLower(id), nil) - triggerauth := &TriggerAuth{} - if err := dbclient.Get(ctx, key, triggerauth); err != nil { - return &TriggerAuth{}, err - } - - return triggerauth, nil -} - -func setTriggerAuth(ctx context.Context, trigger TriggerAuth) error { - key1 := datastore.NameKey("trigger_auth", strings.ToLower(trigger.Id), nil) - - // New struct, to not add body, author etc - if _, err := dbclient.Put(ctx, key1, &trigger); err != nil { - log.Printf("Error adding trigger auth: %s", err) - return err - } - - return nil -} - -// THis all of a sudden became really horrible.. fml -func getOutlookClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) { - - conf := &oauth2.Config{ - ClientID: "", - ClientSecret: "", - Scopes: []string{ - "Mail.Read", - "User.Read", - }, - RedirectURL: redirectUri, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize", - TokenURL: "https://login.microsoftonline.com/common/oauth2/token", - }, - } - - if len(code) > 0 { - access_token, err := conf.Exchange(ctx, code) - if err != nil { - log.Printf("Access_token issue: %s", err) - return &http.Client{}, access_token, err - } - - client := conf.Client(ctx, access_token) - return client, access_token, nil - } else { - // Manually recreate the oauthtoken - access_token := &oauth2.Token{ - AccessToken: accessToken.AccessToken, - TokenType: accessToken.TokenType, - RefreshToken: accessToken.RefreshToken, - Expiry: accessToken.Expiry, - } - - client := conf.Client(ctx, access_token) - return client, access_token, nil - } -} - -func handleGetOutlookFolders(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - // Exchange every time hmm - // FIXME - // Should really just get the code from the trigger that's being used OR the user - triggerId := request.URL.Query().Get("trigger_id") - if len(triggerId) == 0 { - log.Println("No trigger_id supplied") - resp.WriteHeader(401) - return - } - - ctx := context.Background() - trigger, err := getTriggerAuth(ctx, triggerId) - if err != nil { - log.Printf("Trigger %s doesn't exist - outlook folders.", triggerId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Trigger doesn't exist."}`)) - return - } - - // FIXME - should be shuffler in literally every case except testing lol - redirectDomain := "shuffler.io" - url := fmt.Sprintf("https://%s/functions/outlook/register", redirectDomain) - outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("Oauth client failure - outlook folders: %s", err) - resp.WriteHeader(401) - return - } - - folders, err := getOutlookFolders(outlookClient) - if err != nil { - resp.WriteHeader(401) - return - } - - b, err := json.Marshal(folders.Value) - if err != nil { - log.Println("Failed to marshal folderdata") - resp.WriteHeader(401) - return - } - - resp.WriteHeader(200) - resp.Write(b) -} - func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -5395,66 +4918,6 @@ func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(b)) } -func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in getting specific workflow: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - } - - if strings.Contains(workflowId, "?") { - workflowId = strings.Split(workflowId, "?")[0] - } - - ctx := context.Background() - trigger, err := getTriggerAuth(ctx, workflowId) - if err != nil { - log.Printf("Trigger %s doesn't exist - specific trigger.", workflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - if user.Username != trigger.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for trigger %s", user.Username, trigger.Id) - resp.WriteHeader(401) - return - } - - trigger.OauthToken = OauthToken{} - trigger.Code = "" - - b, err := json.Marshal(trigger) - if err != nil { - log.Println("Failed to marshal data") - resp.WriteHeader(401) - return - } - - resp.WriteHeader(200) - resp.Write(b) -} - func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -7545,7 +7008,7 @@ func runInit(ctx context.Context) { if err != nil { log.Printf("Failed loading repo %s into memory: %s", apis, err) } else { - log.Printf("Finished git clone. Looking for updates to the repo.") + log.Printf("[INFO] Finished git clone. Looking for updates to the repo.") dir, err := fs.ReadDir("") if err != nil { log.Printf("Failed reading folder: %s", err) @@ -7856,7 +7319,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { continue } - log.Printf("[INFO] Looking for value %s", value) + log.Printf("[INFO] Looking for value %s in Workflow %s of ORG %s", value, workflowExecution.Workflow.ID, org.Id) q := datastore.NewQuery(dbKey).Filter("org_id =", org.Id).Filter("workflow_id =", workflowExecution.Workflow.ID).Filter("parameter_name =", parameterNames).Filter("value =", value) foundCount, err := dbclient.Count(ctx, q) @@ -7875,7 +7338,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { } } } else { - log.Printf("[INFO] Should validate if value %s in app %s exists WITH ORG %s", workflowExecution.Workflow.ID) + //log.Printf("[INFO] Should validate if value %s in app %s exists WITH ORG %s", workflowExecution.Workflow.ID) for _, value := range value.ParameterValues { if len(value) == 0 { @@ -7883,7 +7346,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { continue } - log.Printf("[INFO] Looking for value %s", value) + log.Printf("[INFO] Looking for value %s in ORG %s", value, org.Id) q := datastore.NewQuery(dbKey).Filter("org_id =", org.Id).Filter("workflow_id =", "").Filter("parameter_name =", parameterNames).Filter("value =", value) foundCount, err := dbclient.Count(ctx, q) @@ -8653,11 +8116,6 @@ func initHandlers() { r.HandleFunc("/api/v1/hooks/{key}", handleWebhookCallback).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/hooks/{key}/delete", handleDeleteHook).Methods("DELETE", "OPTIONS") - // Trigger hmm - //r.HandleFunc("/api/v1/triggers/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") - - r.HandleFunc("/api/v1/stats/{key}", handleGetSpecificStats).Methods("GET", "OPTIONS") - // OpenAPI configuration r.HandleFunc("/api/v1/verify_swagger", verifySwagger).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/verify_openapi", verifySwagger).Methods("POST", "OPTIONS") @@ -8693,6 +8151,12 @@ func initHandlers() { r.HandleFunc("/api/v1/files/{fileId}", handleDeleteFile).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/files", handleGetFiles).Methods("GET", "OPTIONS") + // Trigger hmm + r.HandleFunc("/api/v1/triggers/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/triggers/outlook/getFolders", handleGetOutlookFolders).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/triggers/outlook/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/stats/{key}", handleGetSpecificStats).Methods("GET", "OPTIONS") + http.Handle("/", r) } diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go new file mode 100644 index 00000000..5f234e68 --- /dev/null +++ b/backend/go-app/oauth2.go @@ -0,0 +1,468 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + "cloud.google.com/go/datastore" + "golang.org/x/oauth2" +) + +type OutlookProfile struct { + OdataContext string `json:"@odata.context"` + BusinessPhones []string `json:"businessPhones"` + DisplayName string `json:"displayName"` + GivenName string `json:"givenName"` + JobTitle interface{} `json:"jobTitle"` + Mail string `json:"mail"` + MobilePhone interface{} `json:"mobilePhone"` + OfficeLocation interface{} `json:"officeLocation"` + PreferredLanguage interface{} `json:"preferredLanguage"` + Surname string `json:"surname"` + UserPrincipalName string `json:"userPrincipalName"` + ID string `json:"id"` +} + +type OutlookFolder struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + ParentFolderID string `json:"parentFolderId"` + ChildFolderCount int `json:"childFolderCount"` + UnreadItemCount int `json:"unreadItemCount"` + TotalItemCount int `json:"totalItemCount"` +} + +type OutlookFolders struct { + OdataContext string `json:"@odata.context"` + OdataNextLink string `json:"@odata.nextLink"` + Value []OutlookFolder `json:"value"` +} + +func getOutlookFolders(client *http.Client) (OutlookFolders, error) { + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") + + ret, err := client.Get(requestUrl) + if err != nil { + log.Printf("[INFO] FolderErr: %s", err) + return OutlookFolders{}, err + } + + body, err := ioutil.ReadAll(ret.Body) + if err != nil { + log.Printf("[WARNING] Failed body decoding from mailfolders") + return OutlookFolders{}, err + } + + log.Printf("[INFO] Folder Body: %s", string(body)) + log.Printf("[INFO] Status folders: %d", ret.StatusCode) + if ret.StatusCode != 200 { + return OutlookFolders{}, err + } + + //log.Printf("Body: %s", string(body)) + + mailfolders := OutlookFolders{} + err = json.Unmarshal(body, &mailfolders) + if err != nil { + log.Printf("Unmarshal: %s", err) + return OutlookFolders{}, err + } + + //fmt.Printf("%#v", mailfolders) + // FIXME - recursion for subfolders + // Recursive struct + // folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId) + //for _, folder := range mailfolders.Value { + // log.Println(folder.DisplayName) + //} + + return mailfolders, nil +} + +func getOutlookProfile(client *http.Client) (OutlookProfile, error) { + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me?$select=mail") + + ret, err := client.Get(requestUrl) + if err != nil { + log.Printf("[INFO] Folder error: %s", err) + return OutlookProfile{}, err + } + + log.Printf("[INFO] Status profile: %d", ret.StatusCode) + body, err := ioutil.ReadAll(ret.Body) + if err != nil { + log.Printf("[INFO] Body: %s", err) + return OutlookProfile{}, err + } + + log.Printf("[INFO] BODY: %s", string(body)) + + profile := OutlookProfile{} + err = json.Unmarshal(body, &profile) + if err != nil { + log.Printf("Unmarshal: %s", err) + return OutlookProfile{}, err + } + + return profile, nil +} + +func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { + code := request.URL.Query().Get("code") + if len(code) == 0 { + log.Println("No code") + resp.WriteHeader(401) + return + } + + url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) + log.Println(url) + ctx := context.Background() + client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) + if err != nil { + log.Printf("Oauth client failure - outlook register: %s", err) + resp.WriteHeader(401) + return + } + + // This should be possible, and will also give the actual username + profile, err := getOutlookProfile(client) + if err != nil { + log.Printf("Outlook profile failure: %s", err) + resp.WriteHeader(401) + return + } + + // This is a state workaround, which should really be for CSRF checks lol + state := request.URL.Query().Get("state") + if len(state) == 0 { + log.Println("No state") + resp.WriteHeader(401) + return + } + + stateitems := strings.Split(state, "%26") + if len(stateitems) == 1 { + stateitems = strings.Split(state, "&") + } + + // FIXME - trigger auth + senderUser := "" + trigger := TriggerAuth{} + for _, item := range stateitems { + itemsplit := strings.Split(item, "%3D") + if len(itemsplit) == 1 { + itemsplit = strings.Split(item, "=") + } + + if len(itemsplit) != 2 { + continue + } + + // Do something here + if itemsplit[0] == "workflow_id" { + trigger.WorkflowId = itemsplit[1] + } else if itemsplit[0] == "trigger_id" { + trigger.Id = itemsplit[1] + } else if itemsplit[0] == "type" { + trigger.Type = itemsplit[1] + } else if itemsplit[0] == "username" { + trigger.Username = itemsplit[1] + trigger.Owner = itemsplit[1] + senderUser = itemsplit[1] + } + } + + // THis is an override based on the user in oauth return + trigger.Username = profile.Mail + trigger.Code = code + trigger.OauthToken = OauthToken{ + AccessToken: accessToken.AccessToken, + TokenType: accessToken.TokenType, + RefreshToken: accessToken.RefreshToken, + Expiry: accessToken.Expiry, + } + + //log.Printf("%#v", trigger) + log.Println(trigger.WorkflowId) + log.Println(trigger.Id) + log.Println(trigger.Username) + log.Println(trigger.Type) + if trigger.WorkflowId == "" || trigger.Id == "" || trigger.Username == "" || trigger.Type == "" { + log.Printf("[INFO] All oauth items need to contain data to register a new state") + resp.WriteHeader(401) + return + } + + // Should also update the user + log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) + Userdata, err := getUser(ctx, senderUser) + if err != nil { + log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err) + resp.WriteHeader(401) + return + } + + Userdata.Authentication = append(Userdata.Authentication, UserAuth{ + Name: "Outlook", + Description: "oauth2", + Workflows: []string{trigger.WorkflowId}, + Username: trigger.Username, + Fields: []UserAuthField{ + UserAuthField{ + Key: "trigger_id", + Value: trigger.Id, + }, + UserAuthField{ + Key: "username", + Value: trigger.Username, + }, + UserAuthField{ + Key: "code", + Value: code, + }, + UserAuthField{ + Key: "type", + Value: trigger.Type, + }, + }, + }) + + // Set apikey for the user if they don't have one + err = setUser(ctx, Userdata) + if err != nil { + log.Printf("Failed setting user data for %s: %s", Userdata.Username, err) + resp.WriteHeader(401) + return + } + + err = setTriggerAuth(ctx, trigger) + if err != nil { + log.Printf("Failed to set trigger auth for %s - %s", trigger.Username, err) + resp.WriteHeader(401) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true}`)) +} + +type OauthToken struct { + AccessToken string `json:"AccessToken" datastore:"AccessToken,noindex"` + TokenType string `json:"TokenType" datastore:"TokenType,noindex"` + RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"` + Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"` +} +type TriggerAuth struct { + Id string `json:"id" datastore:"id"` + SubscriptionId string `json:"subscriptionId" datastore:"subscriptionId"` + + Username string `json:"username" datastore:"username,noindex"` + WorkflowId string `json:"workflow_id" datastore:"workflow_id,noindex"` + Owner string `json:"owner" datastore:"owner"` + Type string `json:"type" datastore:"type"` + Code string `json:"code,omitempty" datastore:"code,noindex"` + OauthToken OauthToken `json:"oauth_token,omitempty" datastore:"oauth_token"` +} + +func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { + key := datastore.NameKey("trigger_auth", strings.ToLower(id), nil) + triggerauth := &TriggerAuth{} + if err := dbclient.Get(ctx, key, triggerauth); err != nil { + return &TriggerAuth{}, err + } + + return triggerauth, nil +} + +func setTriggerAuth(ctx context.Context, trigger TriggerAuth) error { + key1 := datastore.NameKey("trigger_auth", strings.ToLower(trigger.Id), nil) + + // New struct, to not add body, author etc + if _, err := dbclient.Put(ctx, key1, &trigger); err != nil { + log.Printf("Error adding trigger auth: %s", err) + return err + } + + return nil +} + +// THis all of a sudden became really horrible.. fml +func getOutlookClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) { + + conf := &oauth2.Config{ + ClientID: "fd55c175-aa30-4fa6-b303-09a29fb3f750", + ClientSecret: "14OBKgUpov.D7fe0~hp0z-cIQdP~SlYm.8", + Scopes: []string{ + "Mail.Read", + }, + RedirectURL: redirectUri, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize", + TokenURL: "https://login.microsoftonline.com/common/oauth2/token", + }, + } + + if len(code) > 0 { + access_token, err := conf.Exchange(ctx, code) + if err != nil { + log.Printf("Access_token issue: %s", err) + return &http.Client{}, access_token, err + } + + client := conf.Client(ctx, access_token) + return client, access_token, nil + } + + // Manually recreate the oauthtoken + access_token := &oauth2.Token{ + AccessToken: accessToken.AccessToken, + TokenType: accessToken.TokenType, + RefreshToken: accessToken.RefreshToken, + Expiry: accessToken.Expiry, + } + + client := conf.Client(ctx, access_token) + return client, access_token, nil +} + +func handleGetOutlookFolders(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + // Exchange every time hmm + // FIXME + // Should really just get the code from the trigger that's being used OR the user + triggerId := request.URL.Query().Get("trigger_id") + if len(triggerId) == 0 { + log.Println("No trigger_id supplied") + resp.WriteHeader(401) + return + } + + ctx := context.Background() + trigger, err := getTriggerAuth(ctx, triggerId) + if err != nil { + log.Printf("[INFO] Trigger %s doesn't exist - outlook folders.", triggerId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Trigger doesn't exist."}`)) + return + } + + //client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) + //if err != nil { + // log.Printf("Oauth client failure - outlook register: %s", err) + // resp.WriteHeader(401) + // return + //} + + // FIXME - should be shuffler in literally every case except testing lol + //log.Printf("TRIGGER: %#v", trigger) + redirectDomain := "localhost:5001" + url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) + if err != nil { + log.Printf("[WARNING] Oauth client failure - outlook folders: %s", err) + resp.Write([]byte(`{"success": false, "reason": "Failed creating outlook client"}`)) + resp.WriteHeader(401) + return + } + + // This should be possible, and will also give the actual username + /* + profile, err := getOutlookProfile(outlookClient) + if err != nil { + log.Printf("Outlook profile failure: %s", err) + resp.WriteHeader(401) + return + } + log.Printf("PROFILE: %#v", profile) + */ + + folders, err := getOutlookFolders(outlookClient) + if err != nil { + log.Printf("[WARNING] Failed setting outlook folders: %s", err) + resp.Write([]byte(`{"success": false, "reason": "Failed getting outlook folders"}`)) + resp.WriteHeader(401) + return + } + + b, err := json.Marshal(folders.Value) + if err != nil { + log.Println("[INFO] Failed to marshal folderdata") + resp.Write([]byte(`{"success": false, "reason": "Failed decoding JSON"}`)) + resp.WriteHeader(401) + return + } + + resp.WriteHeader(200) + resp.Write(b) +} + +func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in getting specific workflow: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + location := strings.Split(request.URL.String(), "/") + + var workflowId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowId = location[5] + } + + if strings.Contains(workflowId, "?") { + workflowId = strings.Split(workflowId, "?")[0] + } + + ctx := context.Background() + trigger, err := getTriggerAuth(ctx, workflowId) + if err != nil { + log.Printf("[INFO] Trigger %s doesn't exist - specific trigger.", workflowId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": ""}`)) + return + } + + if user.Username != trigger.Owner && user.Role != "admin" { + log.Printf("Wrong user (%s) for trigger %s", user.Username, trigger.Id) + resp.WriteHeader(401) + return + } + + trigger.OauthToken = OauthToken{} + trigger.Code = "" + + b, err := json.Marshal(trigger) + if err != nil { + log.Println("Failed to marshal data") + resp.WriteHeader(401) + return + } + + resp.WriteHeader(200) + resp.Write(b) +} diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 3d9923b5..ac3265de 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -982,7 +982,7 @@ func validateNewWorkerExecution(body []byte) error { } if execution.Status == "EXECUTING" { - log.Printf("[INFO] Inside executing.") + //log.Printf("[INFO] Inside executing.") extra := 0 for _, trigger := range execution.Workflow.Triggers { //log.Printf("Appname trigger (0): %s", trigger.AppName) @@ -3992,7 +3992,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally (stop schedule): %s", err) + log.Printf("[WARNING] Failed getting the workflow locally (stop schedule): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4001,7 +4001,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { // FIXME - have a check for org etc too.. // FIXME - admin check like this? idk if user.Id != workflow.Owner && user.Role != "admin" && user.Role != "scheduler" { - log.Printf("Wrong user (%s) for workflow %s (stop schedule)", user.Username, workflow.ID) + log.Printf("[WARNING] Wrong user (%s) for workflow %s (stop schedule)", user.Username, workflow.ID) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4009,7 +4009,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { schedule, err := getSchedule(ctx, scheduleId) if err != nil { - log.Printf("Failed finding schedule %s", scheduleId) + log.Printf("[WARNING] Failed finding schedule %s", scheduleId) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4039,15 +4039,15 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { err = executeCloudAction(action, org.SyncConfig.Apikey) if err != nil { - log.Printf("Failed cloud action STOP schedule: %s", err) + log.Printf("[WARNING] Failed cloud action STOP schedule: %s", err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return } else { - log.Printf("Successfully ran cloud action STOP schedule") + log.Printf("[INFO] Successfully ran cloud action STOP schedule") err = DeleteKey(ctx, "schedules", scheduleId) if err != nil { - log.Printf("Failed deleting cloud schedule onprem..") + log.Printf("[WARNING] Failed deleting cloud schedule onprem..") resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed deleting cloud schedule"}`))) return @@ -4061,7 +4061,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { err = deleteSchedule(ctx, scheduleId) if err != nil { - log.Printf("Failed deleting schedule: %s", err) + log.Printf("[WARNING] Failed deleting schedule: %s", err) if strings.Contains(err.Error(), "Job not found") { resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 45f44ba9..bde4fff9 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2307,10 +2307,12 @@ const AngularWorkflow = (props) => { return } + /* if (data.is_valid === false) { alert.error(data.name+" is only available on https://shuffler.io so far") return } + */ const triggerLabel = getNextActionName(data.name) @@ -4929,7 +4931,7 @@ const AngularWorkflow = (props) => { } const setFolders = () => { - fetch(globalUrl+"/functions/outlook/getFolders?trigger_id="+selectedTrigger.id, { + fetch(globalUrl+"/api/v1/triggers/outlook/getFolders?trigger_id="+selectedTrigger.id, { method: "GET", headers: {"content-type": "application/json"}, credentials: "include", @@ -4942,7 +4944,10 @@ const AngularWorkflow = (props) => { return response.json() }) .then((responseJson) => { - setTriggerFolders(responseJson) + if (responseJson !== null && responseJson.success !== false) { + setTriggerFolders(responseJson) + } + if (workflow.triggers[selectedTriggerIndex].parameters.length === 0 && responseJson.length > 0) { workflow.triggers[selectedTriggerIndex].parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}] selectedTrigger.parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}] @@ -4956,7 +4961,7 @@ const AngularWorkflow = (props) => { } const getTriggerAuth = () => { - fetch(globalUrl+"/api/v1/triggers/"+selectedTrigger.id, { + fetch(globalUrl+"/api/v1/triggers/outlook/"+selectedTrigger.id, { method: "GET", headers: {"content-type": "application/json"}, credentials: "include", @@ -4987,15 +4992,22 @@ const AngularWorkflow = (props) => { const outlookButton =
    {outlookButton} -
    -
    -
    - Folders: (hold CTRL to select multiple) -
    -
    - } - key={selectedTrigger} - > - {triggerFolders.map(folder => { - var folderItem = - if (folder.childFolderCount > 0) { - // Here to handle subfolders sometime later - folderItem = - - } + {triggerFolders === undefined || triggerFolders === null ? null : + +
    +
    +
    + Folders: (hold CTRL to select multiple) +
    +
    + } + key={selectedTrigger} + > + {triggerFolders.map(folder => { + var folderItem = + if (folder.childFolderCount > 0) { + // Here to handle subfolders sometime later + folderItem = + + } - return folderItem - })} - + return folderItem + })} + + + }
    } else if (triggerAuthentication.type === "gmail") { triggerInfo = "SPECIAL GMAIL" @@ -5578,10 +5594,10 @@ const AngularWorkflow = (props) => { if (trigger.id === undefined) { return } - alert.info("Stopping trigger") + alert.info("Deleting mail trigger") fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/outlook/"+trigger.id, { - method: 'DELETE', + method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -5616,6 +5632,10 @@ const AngularWorkflow = (props) => { const startMailSub = (trigger, triggerindex) => { var folders = [] + if (triggerFolders === null || triggerFolders === undefined) { + return null + } + const splitItem = workflow.triggers[selectedTriggerIndex].parameters[0].value.split(splitter) for (var key in splitItem) { const item = splitItem[key] From 9d22a20489c4ce3344fd66a300cc2317897c7016 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Mar 2021 18:49:39 +0100 Subject: [PATCH 165/185] Minor updates --- backend/go-app/main.go | 268 ------------------------------------ backend/go-app/oauth2.go | 283 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 278 insertions(+), 273 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 7dc85deb..29e1432e 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -5057,274 +5057,6 @@ func handleOutlookSubRemoval(ctx context.Context, workflowId, triggerId string) return nil } -// This sets up the sub with outlook itself -// Parses data from the workflow to see whether access is right to subscribe it -// Creates the cloud function for outlook return -// Wait for it to be available, then schedule a workflow to it -func createOutlookSub(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - } - - ctx := context.Background() - workflow, err := getWorkflow(ctx, workflowId) - if err != nil { - log.Printf("Failed getting the workflow locally (outlook sub): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in outlook deploy: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - have a check for org etc too.. - if user.Id != workflow.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - log.Println("Handle outlook subscription for trigger") - - // Should already be authorized at this point, as the workflow is shared - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Failed body read for workflow %s", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - log.Println(string(body)) - - // Based on the input data from frontend - type CurTrigger struct { - Name string `json:"name"` - Folders []string `json:"folders"` - ID string `json:"id"` - } - - var curTrigger CurTrigger - err = json.Unmarshal(body, &curTrigger) - if err != nil { - log.Printf("Failed body read unmarshal for trigger %s", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if len(curTrigger.Folders) == 0 { - log.Printf("Error for %s. Choosing folders is required, currently 0", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Now that it's deployed - wait a few seconds before generating: - // 1. Oauth2 token thingies for outlook.office.com - // 2. Set the url to have the right mailboxes (probably ID?) ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages") - // 3. Set the callback URL to be the new trigger - // 4. Run subscription test - // 5. Set the subscriptionId to the trigger object - - // First - lets regenerate an oauth token for outlook.office.com from the original items - trigger, err := getTriggerAuth(ctx, curTrigger.ID) - if err != nil { - log.Printf("Trigger %s doesn't exist - outlook sub.", curTrigger.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - // url doesn't really matter here - url := fmt.Sprintf("https://shuffler.io") - outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("Oauth client failure - triggerauth: %s", err) - resp.WriteHeader(401) - return - } - - // Location + - notificationURL := fmt.Sprintf("https://%s-%s.cloudfunctions.net/outlooktrigger_%s", defaultLocation, gceProject, curTrigger.ID) - log.Println(notificationURL) - - // This is here simply to let the function start - // Usually takes 10 attempts minimum :O - // 10 * 5 = 50 seconds. That's waaay too much :( - //notificationURL = "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_e2ce43b0-997e-4980-9617-6eadbc68cf88" - //notificationURL = "https://de4fc12b.ngrok.io" - - curSubscriptions, err := getOutlookSubscriptions(outlookClient) - if err == nil { - for _, sub := range curSubscriptions.Value { - if sub.NotificationURL == notificationURL { - log.Printf("Removing existing subscription %s", sub.Id) - removeOutlookSubscription(outlookClient, sub.Id) - } - } - } else { - log.Printf("Failed to get subscriptions - need to overwrite") - } - - maxFails := 15 - failCnt := 0 - log.Println(curTrigger.Folders) - for { - subId, err := makeOutlookSubscription(outlookClient, curTrigger.Folders, notificationURL) - if err != nil { - failCnt += 1 - log.Printf("Failed making oauth subscription, retrying in 5 seconds: %s", err) - time.Sleep(5 * time.Second) - if failCnt == maxFails { - log.Printf("Failed to set up subscription %d times.", maxFails) - resp.WriteHeader(401) - return - } - - continue - } - - // Set the ID somewhere here - trigger.SubscriptionId = subId - err = setTriggerAuth(ctx, *trigger) - if err != nil { - log.Printf("Failed setting triggerauth: %s", err) - } - - break - } - - log.Printf("Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) - - //log.Printf("%#v", user) - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -// Lists the users current subscriptions -func getOutlookSubscriptions(outlookClient *http.Client) (SubscriptionsWrapper, error) { - fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions") - req, err := http.NewRequest( - "GET", - fullUrl, - nil, - ) - req.Header.Add("Content-Type", "application/json") - res, err := outlookClient.Do(req) - if err != nil { - log.Printf("suberror Client: %s", err) - return SubscriptionsWrapper{}, err - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Suberror Body: %s", err) - return SubscriptionsWrapper{}, err - } - - newSubs := SubscriptionsWrapper{} - err = json.Unmarshal(body, &newSubs) - if err != nil { - return SubscriptionsWrapper{}, err - } - - return newSubs, nil -} - -type SubscriptionsWrapper struct { - OdataContext string `json:"@odata.context"` - Value []Subscription `json:"value"` -} - -type Subscription struct { - ChangeType string `json:"changeType"` - NotificationURL string `json:"notificationUrl"` - Resource string `json:"resource"` - ExpirationDateTime string `json:"expirationDateTime"` - ClientState string `json:"clientState"` - Id string `json:"id"` -} - -func makeOutlookSubscription(client *http.Client, folderIds []string, notificationURL string) (string, error) { - fullUrl := "https://graph.microsoft.com/v1.0/subscriptions" - - // FIXME - this expires rofl - t := time.Now().Local().Add(time.Minute * time.Duration(4300)) - timeFormat := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.0000000Z", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) - log.Println(timeFormat) - - resource := fmt.Sprintf("me/mailfolders('%s')/messages", strings.Join(folderIds, "','")) - log.Println(resource) - sub := Subscription{ - ChangeType: "created", - NotificationURL: notificationURL, - ExpirationDateTime: timeFormat, - ClientState: "This is a test", - Resource: resource, - } - - data, err := json.Marshal(sub) - if err != nil { - log.Printf("Marshal: %s", err) - return "", err - } - - req, err := http.NewRequest( - "POST", - fullUrl, - bytes.NewBuffer(data), - ) - req.Header.Add("Content-Type", "application/json") - - res, err := client.Do(req) - if err != nil { - log.Printf("Client: %s", err) - return "", err - } - - log.Printf("Status: %d", res.StatusCode) - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Body: %s", err) - return "", err - } - - if res.StatusCode != 200 && res.StatusCode != 201 { - return "", errors.New(fmt.Sprintf("Subscription failed: %s", string(body))) - } - - // Use data from body here to create thingy - newSub := Subscription{} - err = json.Unmarshal(body, &newSub) - if err != nil { - return "", err - } - - return newSub.Id, nil -} - func getOpenapi(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go index 5f234e68..598c7b27 100644 --- a/backend/go-app/oauth2.go +++ b/backend/go-app/oauth2.go @@ -1,8 +1,10 @@ package main import ( + "bytes" "context" "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -45,7 +47,8 @@ type OutlookFolders struct { } func getOutlookFolders(client *http.Client) (OutlookFolders, error) { - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") + //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/mailFolders") ret, err := client.Get(requestUrl) if err != nil { @@ -59,7 +62,7 @@ func getOutlookFolders(client *http.Client) (OutlookFolders, error) { return OutlookFolders{}, err } - log.Printf("[INFO] Folder Body: %s", string(body)) + //log.Printf("[INFO] Folder Body: %s", string(body)) log.Printf("[INFO] Status folders: %d", ret.StatusCode) if ret.StatusCode != 200 { return OutlookFolders{}, err @@ -165,6 +168,8 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { continue } + log.Printf("ITEM: %#v", itemsplit) + // Do something here if itemsplit[0] == "workflow_id" { trigger.WorkflowId = itemsplit[1] @@ -192,16 +197,16 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { //log.Printf("%#v", trigger) log.Println(trigger.WorkflowId) log.Println(trigger.Id) - log.Println(trigger.Username) + log.Println(senderUser) log.Println(trigger.Type) - if trigger.WorkflowId == "" || trigger.Id == "" || trigger.Username == "" || trigger.Type == "" { + log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) + if trigger.WorkflowId == "" || trigger.Id == "" || senderUser == "" || trigger.Type == "" { log.Printf("[INFO] All oauth items need to contain data to register a new state") resp.WriteHeader(401) return } // Should also update the user - log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) Userdata, err := getUser(ctx, senderUser) if err != nil { log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err) @@ -466,3 +471,271 @@ func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { resp.WriteHeader(200) resp.Write(b) } + +// This sets up the sub with outlook itself +// Parses data from the workflow to see whether access is right to subscribe it +// Creates the cloud function for outlook return +// Wait for it to be available, then schedule a workflow to it +func createOutlookSub(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + location := strings.Split(request.URL.String(), "/") + + var workflowId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowId = location[4] + } + + ctx := context.Background() + workflow, err := getWorkflow(ctx, workflowId) + if err != nil { + log.Printf("Failed getting the workflow locally (outlook sub): %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in outlook deploy: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // FIXME - have a check for org etc too.. + if user.Id != workflow.Owner && user.Role != "admin" { + log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + log.Println("Handle outlook subscription for trigger") + + // Should already be authorized at this point, as the workflow is shared + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("Failed body read for workflow %s", workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + log.Println(string(body)) + + // Based on the input data from frontend + type CurTrigger struct { + Name string `json:"name"` + Folders []string `json:"folders"` + ID string `json:"id"` + } + + var curTrigger CurTrigger + err = json.Unmarshal(body, &curTrigger) + if err != nil { + log.Printf("Failed body read unmarshal for trigger %s", workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if len(curTrigger.Folders) == 0 { + log.Printf("Error for %s. Choosing folders is required, currently 0", workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // Now that it's deployed - wait a few seconds before generating: + // 1. Oauth2 token thingies for outlook.office.com + // 2. Set the url to have the right mailboxes (probably ID?) ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages") + // 3. Set the callback URL to be the new trigger + // 4. Run subscription test + // 5. Set the subscriptionId to the trigger object + + // First - lets regenerate an oauth token for outlook.office.com from the original items + trigger, err := getTriggerAuth(ctx, curTrigger.ID) + if err != nil { + log.Printf("Trigger %s doesn't exist - outlook sub.", curTrigger.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": ""}`)) + return + } + + // url doesn't really matter here + url := fmt.Sprintf("https://shuffler.io") + outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) + if err != nil { + log.Printf("Oauth client failure - triggerauth: %s", err) + resp.WriteHeader(401) + return + } + + // Location + + notificationURL := fmt.Sprintf("https://%s-%s.cloudfunctions.net/outlooktrigger_%s", defaultLocation, gceProject, curTrigger.ID) + log.Println(notificationURL) + + // This is here simply to let the function start + // Usually takes 10 attempts minimum :O + // 10 * 5 = 50 seconds. That's waaay too much :( + //notificationURL = "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_e2ce43b0-997e-4980-9617-6eadbc68cf88" + //notificationURL = "https://de4fc12b.ngrok.io" + + curSubscriptions, err := getOutlookSubscriptions(outlookClient) + if err == nil { + for _, sub := range curSubscriptions.Value { + if sub.NotificationURL == notificationURL { + log.Printf("Removing existing subscription %s", sub.Id) + removeOutlookSubscription(outlookClient, sub.Id) + } + } + } else { + log.Printf("Failed to get subscriptions - need to overwrite") + } + + maxFails := 15 + failCnt := 0 + log.Println(curTrigger.Folders) + for { + subId, err := makeOutlookSubscription(outlookClient, curTrigger.Folders, notificationURL) + if err != nil { + failCnt += 1 + log.Printf("Failed making oauth subscription, retrying in 5 seconds: %s", err) + time.Sleep(5 * time.Second) + if failCnt == maxFails { + log.Printf("Failed to set up subscription %d times.", maxFails) + resp.WriteHeader(401) + return + } + + continue + } + + // Set the ID somewhere here + trigger.SubscriptionId = subId + err = setTriggerAuth(ctx, *trigger) + if err != nil { + log.Printf("Failed setting triggerauth: %s", err) + } + + break + } + + log.Printf("Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) + + //log.Printf("%#v", user) + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true}`)) +} + +// Lists the users current subscriptions +func getOutlookSubscriptions(outlookClient *http.Client) (SubscriptionsWrapper, error) { + fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions") + req, err := http.NewRequest( + "GET", + fullUrl, + nil, + ) + req.Header.Add("Content-Type", "application/json") + res, err := outlookClient.Do(req) + if err != nil { + log.Printf("suberror Client: %s", err) + return SubscriptionsWrapper{}, err + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("Suberror Body: %s", err) + return SubscriptionsWrapper{}, err + } + + newSubs := SubscriptionsWrapper{} + err = json.Unmarshal(body, &newSubs) + if err != nil { + return SubscriptionsWrapper{}, err + } + + return newSubs, nil +} + +type SubscriptionsWrapper struct { + OdataContext string `json:"@odata.context"` + Value []Subscription `json:"value"` +} + +type Subscription struct { + ChangeType string `json:"changeType"` + NotificationURL string `json:"notificationUrl"` + Resource string `json:"resource"` + ExpirationDateTime string `json:"expirationDateTime"` + ClientState string `json:"clientState"` + Id string `json:"id"` +} + +func makeOutlookSubscription(client *http.Client, folderIds []string, notificationURL string) (string, error) { + fullUrl := "https://graph.microsoft.com/v1.0/subscriptions" + + // FIXME - this expires rofl + t := time.Now().Local().Add(time.Minute * time.Duration(4300)) + timeFormat := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.0000000Z", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) + log.Println(timeFormat) + + resource := fmt.Sprintf("me/mailfolders('%s')/messages", strings.Join(folderIds, "','")) + log.Println(resource) + sub := Subscription{ + ChangeType: "created", + NotificationURL: notificationURL, + ExpirationDateTime: timeFormat, + ClientState: "This is a test", + Resource: resource, + } + + data, err := json.Marshal(sub) + if err != nil { + log.Printf("Marshal: %s", err) + return "", err + } + + req, err := http.NewRequest( + "POST", + fullUrl, + bytes.NewBuffer(data), + ) + req.Header.Add("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + log.Printf("Client: %s", err) + return "", err + } + + log.Printf("Status: %d", res.StatusCode) + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("Body: %s", err) + return "", err + } + + if res.StatusCode != 200 && res.StatusCode != 201 { + return "", errors.New(fmt.Sprintf("Subscription failed: %s", string(body))) + } + + // Use data from body here to create thingy + newSub := Subscription{} + err = json.Unmarshal(body, &newSub) + if err != nil { + return "", err + } + + return newSub.Id, nil +} From d662e9d6aa564c50eba7b5cab97e2e378d44f74e Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 14 Mar 2021 17:20:41 +0100 Subject: [PATCH 166/185] Fixed last pieces for oauth and app integrations --- backend/app_sdk/app_base.py | 2 +- backend/app_sdk/build.sh | 2 +- backend/go-app/main.go | 210 +++------- backend/go-app/oauth2.go | 519 +++++++++++++++++++++++-- backend/go-app/walkoff.go | 12 +- frontend/src/views/AngularWorkflow.jsx | 138 ++++--- 6 files changed, 641 insertions(+), 242 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index b9eee023..4db578cb 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1743,7 +1743,7 @@ class AppBase: # Custom format for ${name[0,1,2,...]}$ #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" - print(f"Returnedvalue: {value}") + #print(f"Returnedvalue: {value}") # OLD: Used until 13.03.2021: submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" # \${[0-9a-zA-Z_-]+#?(\[.*?]}\$) submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*?]}\$))" diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 258a65db..eaee980c 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.63 +VERSION=0.8.64 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 29e1432e..1b6be031 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -73,11 +73,13 @@ var bucketName = "shuffler.appspot.com" var baseAppPath = "/home/frikky/git/shaffuru/tmp/apps" var baseDockerName = "frikky/shuffle" var registryName = "registry.hub.docker.com" +var runningEnvironment = "onprem" -//var syncUrl = "http://192.168.102.54:5002" var syncUrl = "https://shuffler.io" +var syncSubUrl = "https://shuffler.io" //var syncUrl = "http://localhost:5002" +//var syncSubUrl = "https://050196912a9d.ngrok.io" var dbclient *datastore.Client var requestCache *cache.Cache @@ -3478,10 +3480,12 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // OrgId: activeOrgs[0].Id, workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest) if err == nil { - err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) - if err != nil { - log.Printf("Failed to increase total apps loaded stats: %s", err) - } + /* + err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) + if err != nil { + log.Printf("Failed to increase total apps loaded stats: %s", err) + } + */ resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) @@ -4918,145 +4922,6 @@ func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(b)) } -func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - var triggerId string - if location[1] == "api" { - if len(location) <= 6 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - triggerId = location[6] - } - - if len(workflowId) == 0 || len(triggerId) == 0 { - log.Printf("Ids can't be zero when deleting %s", workflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - ctx := context.Background() - workflow, err := getWorkflow(ctx, workflowId) - if err != nil { - log.Printf("Failed getting the workflow locally (delete outlook): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in outlook deploy: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - have a check for org etc too.. - if user.Id != workflow.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Check what kind of sub it is - err = handleOutlookSubRemoval(ctx, workflowId, triggerId) - if err != nil { - log.Printf("Failed sub removal: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func removeOutlookSubscription(outlookClient *http.Client, subscriptionId string) error { - // DELETE https://graph.microsoft.com/v1.0/subscriptions/{id} - fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions/%s", subscriptionId) - req, err := http.NewRequest( - "DELETE", - fullUrl, - nil, - ) - req.Header.Add("Content-Type", "application/json") - res, err := outlookClient.Do(req) - if err != nil { - log.Printf("Client: %s", err) - return err - } - - if res.StatusCode != 200 && res.StatusCode != 201 && res.StatusCode != 204 { - return errors.New(fmt.Sprintf("Bad status code when deleting subscription: %d", res.StatusCode)) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Body: %s", err) - return err - } - - _ = body - - return nil -} - -// Remove AUTH -// Remove function -// Remove subscription -func handleOutlookSubRemoval(ctx context.Context, workflowId, triggerId string) error { - // 1. Get the auth for trigger - // 2. Stop the subscription - // 3. Remove the function - // 4. Remove the database entry for auth - trigger, err := getTriggerAuth(ctx, triggerId) - if err != nil { - log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId) - return err - } - - url := fmt.Sprintf("https://shuffler.io") - outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("Oauth client failure - triggerauth sub removal: %s", err) - return err - } - - notificationURL := fmt.Sprintf("https://%s-%s.cloudfunctions.net/outlooktrigger_%s", defaultLocation, gceProject, trigger.Id) - curSubscriptions, err := getOutlookSubscriptions(outlookClient) - if err == nil { - for _, sub := range curSubscriptions.Value { - if sub.NotificationURL == notificationURL { - log.Printf("Removing existing subscription %s", sub.Id) - removeOutlookSubscription(outlookClient, sub.Id) - } - } - } else { - log.Printf("Failed to get subscriptions - need to overwrite") - } - - // FIXME - not removing the function, as the trigger still exists - //err = removeOutlookTriggerFunction(triggerId) - //if err != nil { - // return err - //} - - return nil -} - func getOpenapi(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -5971,8 +5836,58 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio func handleCloudJob(job CloudSyncJob) error { // May need authentication in all of these..? - log.Printf("Handle job with type %s and action %s", job.Type, job.Action) - if job.Type == "webhook" { + log.Printf("[INFO] Handle job with type %s and action %s", job.Type, job.Action) + if job.Type == "outlook" { + if job.Action == "execute" { + // FIXME: Get the email + ctx := context.Background() + maildata := MailData{} + err := json.Unmarshal([]byte(job.ThirdItem), &maildata) + if err != nil { + log.Printf("Maildata unmarshal error: %s", err) + return err + } + + hookId := job.Id + hook, err := getTriggerAuth(ctx, hookId) + if err != nil { + log.Printf("[INFO] Failed getting trigger %s (callback cloud): %s", hookId, err) + return err + } + + redirectDomain := "localhost:5001" + redirectUrl := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", hook.OauthToken, redirectUrl) + if err != nil { + log.Printf("Oauth client failure - triggerauth: %s", err) + return err + } + + emails, err := getOutlookEmail(outlookClient, maildata) + log.Printf("EMAILS: %d", len(emails)) + log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err) + + //type FullEmail struct { + email := FullEmail{} + if len(emails) == 1 { + email = emails[0] + } + + emailBytes, err := json.Marshal(email) + if err != nil { + log.Printf("[INFO] Failed email marshaling: %s", err) + return err + } + + log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err = handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "outlook", string(emailBytes)) + if err != nil { + log.Printf("Failed executing workflow from cloud outlook hook: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud outlook hook!") + } + } + } else if job.Type == "webhook" { if job.Action == "execute" { log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "webhook", job.ThirdItem) @@ -6138,7 +6053,7 @@ func remoteOrgJobController(org Org, body []byte) error { } if len(responseData.Jobs) > 0 { - log.Printf("Remote JOB ret: %s", string(body)) + log.Printf("[INFO] Remote JOB ret: %s", string(body)) log.Printf("Got job with reason %s and %d job(s)", responseData.Reason, len(responseData.Jobs)) } @@ -7887,6 +7802,7 @@ func initHandlers() { r.HandleFunc("/api/v1/triggers/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/outlook/getFolders", handleGetOutlookFolders).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/outlook/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/triggers/outlook/{key}/callback", handleOutlookCallback).Methods("POST", "OPTIONS") //r.HandleFunc("/api/v1/stats/{key}", handleGetSpecificStats).Methods("GET", "OPTIONS") http.Handle("/", r) diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go index 598c7b27..04bb845c 100644 --- a/backend/go-app/oauth2.go +++ b/backend/go-app/oauth2.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "strings" "time" @@ -16,6 +17,77 @@ import ( "golang.org/x/oauth2" ) +type FullEmail struct { + OdataContext string `json:"@odata.context"` + OdataEtag string `json:"@odata.etag"` + ID string `json:"id"` + Createddatetime time.Time `json:"createdDateTime"` + Lastmodifieddatetime time.Time `json:"lastModifiedDateTime"` + Changekey string `json:"changeKey"` + Categories []interface{} `json:"categories"` + Receiveddatetime time.Time `json:"receivedDateTime"` + Sentdatetime time.Time `json:"sentDateTime"` + Hasattachments bool `json:"hasAttachments"` + Internetmessageid string `json:"internetMessageId"` + Subject string `json:"subject"` + Bodypreview string `json:"bodyPreview"` + Importance string `json:"importance"` + Parentfolderid string `json:"parentFolderId"` + Conversationid string `json:"conversationId"` + Conversationindex string `json:"conversationIndex"` + Isdeliveryreceiptrequested interface{} `json:"isDeliveryReceiptRequested"` + Isreadreceiptrequested bool `json:"isReadReceiptRequested"` + Isread bool `json:"isRead"` + Isdraft bool `json:"isDraft"` + Weblink string `json:"webLink"` + Inferenceclassification string `json:"inferenceClassification"` + Body struct { + Contenttype string `json:"contentType"` + Content string `json:"content"` + } `json:"body"` + Sender struct { + Emailaddress struct { + Name string `json:"name"` + Address string `json:"address"` + } `json:"emailAddress"` + } `json:"sender"` + From struct { + Emailaddress struct { + Name string `json:"name"` + Address string `json:"address"` + } `json:"emailAddress"` + } `json:"from"` + Torecipients []struct { + Emailaddress struct { + Name string `json:"name"` + Address string `json:"address"` + } `json:"emailAddress"` + } `json:"toRecipients"` + Ccrecipients []interface{} `json:"ccRecipients"` + Bccrecipients []interface{} `json:"bccRecipients"` + Replyto []interface{} `json:"replyTo"` + Flag struct { + Flagstatus string `json:"flagStatus"` + } `json:"flag"` +} + +type MailData struct { + Value []struct { + Subscriptionid string `json:"subscriptionId"` + Subscriptionexpirationdatetime string `json:"subscriptionExpirationDateTime"` + Changetype string `json:"changeType"` + Resource string `json:"resource"` + Resourcedata struct { + OdataType string `json:"@odata.type"` + OdataID string `json:"@odata.id"` + OdataEtag string `json:"@odata.etag"` + ID string `json:"id"` + } `json:"resourceData"` + Clientstate string `json:"clientState"` + Tenantid string `json:"tenantId"` + } `json:"value"` +} + type OutlookProfile struct { OdataContext string `json:"@odata.context"` BusinessPhones []string `json:"businessPhones"` @@ -46,6 +118,50 @@ type OutlookFolders struct { Value []OutlookFolder `json:"value"` } +func getOutlookEmail(client *http.Client, maildata MailData) ([]FullEmail, error) { + //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") + + emails := []FullEmail{} + for _, email := range maildata.Value { + //messageId := email.Resourcedata.ID + //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/%s", messageId) + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s", email.Resource) + log.Printf("URL: %#v", requestUrl) + + ret, err := client.Get(requestUrl) + if err != nil { + log.Printf("[INFO] OutlookErr: %s", err) + return []FullEmail{}, err + } + + body, err := ioutil.ReadAll(ret.Body) + if err != nil { + log.Printf("[WARNING] Failed body decoding from outlook email") + return []FullEmail{}, err + } + + //type FullEmail struct { + log.Printf("[INFO] EMAIL Body: %s", string(body)) + log.Printf("[INFO] Status email: %d", ret.StatusCode) + if ret.StatusCode != 200 { + return []FullEmail{}, err + } + + //log.Printf("Body: %s", string(body)) + + parsedmail := FullEmail{} + err = json.Unmarshal(body, &parsedmail) + if err != nil { + log.Printf("[INFO] Email unmarshal error: %s", err) + return []FullEmail{}, err + } + + emails = append(emails, parsedmail) + } + + return emails, nil +} + func getOutlookFolders(client *http.Client) (OutlookFolders, error) { //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/mailFolders") @@ -127,7 +243,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) log.Println(url) ctx := context.Background() - client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) + _, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) if err != nil { log.Printf("Oauth client failure - outlook register: %s", err) resp.WriteHeader(401) @@ -135,12 +251,15 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { } // This should be possible, and will also give the actual username - profile, err := getOutlookProfile(client) - if err != nil { - log.Printf("Outlook profile failure: %s", err) - resp.WriteHeader(401) - return - } + + /* + profile, err := getOutlookProfile(client) + if err != nil { + log.Printf("Outlook profile failure: %s", err) + resp.WriteHeader(401) + return + } + */ // This is a state workaround, which should really be for CSRF checks lol state := request.URL.Query().Get("state") @@ -168,7 +287,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { continue } - log.Printf("ITEM: %#v", itemsplit) + //log.Printf("ITEM: %#v", itemsplit) // Do something here if itemsplit[0] == "workflow_id" { @@ -177,6 +296,8 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { trigger.Id = itemsplit[1] } else if itemsplit[0] == "type" { trigger.Type = itemsplit[1] + } else if itemsplit[0] == "start" { + trigger.Start = itemsplit[1] } else if itemsplit[0] == "username" { trigger.Username = itemsplit[1] trigger.Owner = itemsplit[1] @@ -185,7 +306,12 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { } // THis is an override based on the user in oauth return - trigger.Username = profile.Mail + /* + if len(profile.Mail) > 0 { + trigger.Username = profile.Mail + } + */ + trigger.Code = code trigger.OauthToken = OauthToken{ AccessToken: accessToken.AccessToken, @@ -199,6 +325,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { log.Println(trigger.Id) log.Println(senderUser) log.Println(trigger.Type) + log.Printf("STARTNODE: %s", trigger.Start) log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) if trigger.WorkflowId == "" || trigger.Id == "" || senderUser == "" || trigger.Type == "" { log.Printf("[INFO] All oauth items need to contain data to register a new state") @@ -264,6 +391,7 @@ type OauthToken struct { RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"` Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"` } + type TriggerAuth struct { Id string `json:"id" datastore:"id"` SubscriptionId string `json:"subscriptionId" datastore:"subscriptionId"` @@ -273,6 +401,7 @@ type TriggerAuth struct { Owner string `json:"owner" datastore:"owner"` Type string `json:"type" datastore:"type"` Code string `json:"code,omitempty" datastore:"code,noindex"` + Start string `json:"start" datastore:"start"` OauthToken OauthToken `json:"oauth_token,omitempty" datastore:"oauth_token"` } @@ -520,7 +649,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { return } - log.Println("Handle outlook subscription for trigger") + log.Println("[INFO] Handle outlook subscription for trigger") // Should already be authorized at this point, as the workflow is shared body, err := ioutil.ReadAll(request.Body) @@ -531,8 +660,6 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { return } - log.Println(string(body)) - // Based on the input data from frontend type CurTrigger struct { Name string `json:"name"` @@ -540,6 +667,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { ID string `json:"id"` } + //log.Println(string(body)) var curTrigger CurTrigger err = json.Unmarshal(body, &curTrigger) if err != nil { @@ -566,44 +694,74 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { // First - lets regenerate an oauth token for outlook.office.com from the original items trigger, err := getTriggerAuth(ctx, curTrigger.ID) if err != nil { - log.Printf("Trigger %s doesn't exist - outlook sub.", curTrigger.ID) + log.Printf("[INFO] Trigger %s doesn't exist - outlook sub.", curTrigger.ID) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": ""}`)) return } // url doesn't really matter here - url := fmt.Sprintf("https://shuffler.io") + //url := fmt.Sprintf("https://shuffler.io") + redirectDomain := "localhost:5001" + url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) if err != nil { log.Printf("Oauth client failure - triggerauth: %s", err) + resp.Write([]byte(`{"success": false, "reason": ""}`)) resp.WriteHeader(401) return } // Location + - notificationURL := fmt.Sprintf("https://%s-%s.cloudfunctions.net/outlooktrigger_%s", defaultLocation, gceProject, curTrigger.ID) - log.Println(notificationURL) // This is here simply to let the function start // Usually takes 10 attempts minimum :O // 10 * 5 = 50 seconds. That's waaay too much :( - //notificationURL = "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_e2ce43b0-997e-4980-9617-6eadbc68cf88" - //notificationURL = "https://de4fc12b.ngrok.io" + if runningEnvironment != "cloud" { + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + log.Printf("[INFO] Starting cloud configuration TO STOP trigger %s in org %s", trigger.Id, org.Id) + + action := CloudSyncJob{ + Type: "outlook", + Action: "start", + OrgId: org.Id, + PrimaryItemId: trigger.Id, + SecondaryItem: trigger.Start, + ThirdItem: trigger.WorkflowId, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("[INFO] Failed cloud action START outlook execution: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("[INFO] Successfully set up cloud action trigger") + } + } else { + log.Printf("Should configure a running environment for CLOUD") + } + + notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) curSubscriptions, err := getOutlookSubscriptions(outlookClient) if err == nil { for _, sub := range curSubscriptions.Value { if sub.NotificationURL == notificationURL { - log.Printf("Removing existing subscription %s", sub.Id) + log.Printf("[INFO] Removing existing subscription %s", sub.Id) removeOutlookSubscription(outlookClient, sub.Id) } } } else { - log.Printf("Failed to get subscriptions - need to overwrite") + log.Printf("[INFO] Failed to get subscriptions - need to overwrite") } - maxFails := 15 + maxFails := 5 failCnt := 0 log.Println(curTrigger.Folders) for { @@ -631,7 +789,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { break } - log.Printf("Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) + log.Printf("[INFO] Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) //log.Printf("%#v", user) resp.WriteHeader(200) @@ -686,19 +844,19 @@ func makeOutlookSubscription(client *http.Client, folderIds []string, notificati fullUrl := "https://graph.microsoft.com/v1.0/subscriptions" // FIXME - this expires rofl - t := time.Now().Local().Add(time.Minute * time.Duration(4300)) + t := time.Now().Local().Add(time.Minute * time.Duration(4200)) timeFormat := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.0000000Z", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) - log.Println(timeFormat) resource := fmt.Sprintf("me/mailfolders('%s')/messages", strings.Join(folderIds, "','")) - log.Println(resource) + log.Printf("[INFO] Subscription resource to get(s): %s", resource) sub := Subscription{ ChangeType: "created", + ClientState: "Shuffle subscription", NotificationURL: notificationURL, ExpirationDateTime: timeFormat, - ClientState: "This is a test", Resource: resource, } + //ClientState: "This is a test", data, err := json.Marshal(sub) if err != nil { @@ -719,7 +877,7 @@ func makeOutlookSubscription(client *http.Client, folderIds []string, notificati return "", err } - log.Printf("Status: %d", res.StatusCode) + log.Printf("[INFO] Subscription Status: %d", res.StatusCode) body, err := ioutil.ReadAll(res.Body) if err != nil { log.Printf("Body: %s", err) @@ -739,3 +897,310 @@ func makeOutlookSubscription(client *http.Client, folderIds []string, notificati return newSub.Id, nil } + +// Basically the same as a webhook +func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) { + path := strings.Split(request.URL.String(), "/") + if len(path) < 4 { + log.Printf("[INFO] Bad outlook callback URL: %s", path) + resp.WriteHeader(403) + resp.Write([]byte(`{"success": false}`)) + return + } + + // 1. Get config with hookId + //fmt.Sprintf("%s/api/v1/hooks/%s", callbackUrl, hookId) + ctx := context.Background() + location := strings.Split(request.URL.String(), "/") + + var hookId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + hookId = location[5] + } + + // ID: webhook_ + if len(hookId) != 36 { + log.Printf("[WARNING] Bad hook ID: %s (%d)", hookId, len(hookId)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "message": "ID not valid"}`)) + return + } + + //func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { + hook, err := getTriggerAuth(ctx, hookId) + if err != nil { + log.Printf("[INFO] Failed getting trigger %s (callback): %s", hookId, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("[INFO] Body data error: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + //log.Printf("[INFO] BODY: %s. Len: %d", string(body), len(body)) + //key, ok := request.URL.Query()["validationToken"] + //if ok { + //} + token := request.URL.Query().Get("validationToken") + if len(body) == 0 && len(token) > 0 { + log.Printf("[INFO] Should handle trigger token %s", token) + resp.WriteHeader(200) + resp.Write([]byte(string(token))) + return + } + + // 1. Take the body and parse data -> Get the email itself + + maildata := MailData{} + err = json.Unmarshal(body, &maildata) + if err != nil { + log.Printf("Maildata unmarshal error: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + redirectDomain := "localhost:5001" + redirectUrl := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", hook.OauthToken, redirectUrl) + if err != nil { + log.Printf("Oauth client failure - triggerauth: %s", err) + resp.WriteHeader(401) + return + } + + emails, err := getOutlookEmail(outlookClient, maildata) + log.Printf("EMAILS: %d", len(emails)) + log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err) + + //type FullEmail struct { + email := FullEmail{} + if len(emails) == 1 { + email = emails[0] + } + + emailBytes, err := json.Marshal(email) + if err != nil { + log.Printf("[INFO] Failed email marshaling: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + type ExecutionStruct struct { + Start string `json:"start"` + ExecutionSource string `json:"execution_source"` + ExecutionArgument string `json:"execution_argument"` + } + + newBody := ExecutionStruct{ + Start: hook.Start, + ExecutionSource: "outlook", + ExecutionArgument: string(emailBytes), + } + + b, err := json.Marshal(newBody) + if err != nil { + log.Printf("[INFO] Failed newBody marshaling: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + baseUrl := &url.URL{} + newRequest := &http.Request{ + URL: baseUrl, + Method: "POST", + Body: ioutil.NopCloser(bytes.NewReader(b)), + } + + workflow := Workflow{ + ID: "", + } + + // OrgId: activeOrgs[0].Id, + workflowExecution, executionResp, err := handleExecution(hook.WorkflowId, workflow, newRequest) + if err == nil { + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) + return + } + + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) +} + +func removeOutlookSubscription(outlookClient *http.Client, subscriptionId string) error { + // DELETE https://graph.microsoft.com/v1.0/subscriptions/{id} + fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions/%s", subscriptionId) + req, err := http.NewRequest( + "DELETE", + fullUrl, + nil, + ) + req.Header.Add("Content-Type", "application/json") + res, err := outlookClient.Do(req) + if err != nil { + log.Printf("Client: %s", err) + return err + } + + if res.StatusCode != 200 && res.StatusCode != 201 && res.StatusCode != 204 { + return errors.New(fmt.Sprintf("Bad status code when deleting subscription: %d", res.StatusCode)) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("Body: %s", err) + return err + } + + _ = body + + return nil +} + +// Remove AUTH +// Remove function +// Remove subscription +func handleOutlookSubRemoval(ctx context.Context, user User, workflowId, triggerId string) error { + // 1. Get the auth for trigger + // 2. Stop the subscription + // 3. Remove the function + // 4. Remove the database entry for auth + trigger, err := getTriggerAuth(ctx, triggerId) + if err != nil { + log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId) + return err + } + + if runningEnvironment != "cloud" { + log.Printf("[INFO] SHOULD STOP OUTLOOK SUB ONPREM SYNC WITH CLOUD") + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("[INFO] Failed finding org %s during outlook removal: %s", org.Id, err) + return err + } + + log.Printf("[INFO] Stopping cloud configuration for trigger %s in org %s", trigger.Id, org.Id) + action := CloudSyncJob{ + Type: "outlook", + Action: "stop", + OrgId: org.Id, + PrimaryItemId: trigger.Id, + SecondaryItem: trigger.Start, + ThirdItem: trigger.WorkflowId, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("[INFO] Failed cloud action STOP outlook execution: %s", err) + return err + } else { + log.Printf("[INFO] Successfully set STOPPED outlook execution trigger") + } + } else { + log.Printf("SHOULD STOP OUTLOOK SUB IN CLOUD") + } + + // Actually delete the thing + redirectDomain := "localhost:5001" + url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) + if err != nil { + log.Printf("[WARNING] Oauth client failure - outlook folders: %s", err) + return err + } + notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) + curSubscriptions, err := getOutlookSubscriptions(outlookClient) + if err == nil { + for _, sub := range curSubscriptions.Value { + if sub.NotificationURL == notificationURL { + log.Printf("[INFO] Removing subscription %s from o365", sub.Id) + removeOutlookSubscription(outlookClient, sub.Id) + } + } + } else { + log.Printf("Failed to get subscriptions - need to overwrite") + } + + return nil +} + +func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + location := strings.Split(request.URL.String(), "/") + + var workflowId string + var triggerId string + if location[1] == "api" { + if len(location) <= 6 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowId = location[4] + triggerId = location[6] + } + + if len(workflowId) == 0 || len(triggerId) == 0 { + log.Printf("Ids can't be zero when deleting %s", workflowId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + ctx := context.Background() + workflow, err := getWorkflow(ctx, workflowId) + if err != nil { + log.Printf("Failed getting the workflow locally (delete outlook): %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in outlook deploy: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // FIXME - have a check for org etc too.. + if user.Id != workflow.Owner && user.Role != "admin" { + log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // Check what kind of sub it is + err = handleOutlookSubRemoval(ctx, user, workflowId, triggerId) + if err != nil { + log.Printf("Failed sub removal: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true}`)) +} diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index ac3265de..b89d079d 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2034,7 +2034,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { // log.Printf("Failed to delete webhook: %s", err) //} } else if item.TriggerType == "EMAIL" { - err = handleOutlookSubRemoval(ctx, workflow.ID, item.ID) + err = handleOutlookSubRemoval(ctx, user, workflow.ID, item.ID) if err != nil { log.Printf("Failed to delete email sub: %s", err) } @@ -2224,7 +2224,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("PRE BODY") + //log.Printf("PRE BODY") body, err := ioutil.ReadAll(request.Body) if err != nil { log.Printf("Failed hook unmarshaling: %s", err) @@ -2268,7 +2268,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { allNodes := []string{} workflow.Categories = Categories{} - log.Printf("PRE APPS") + //log.Printf("PRE APPS") workflowapps, apperr := getAllWorkflowApps(ctx, 500) //log.Printf("Action: %#v", action.Authentication) @@ -2553,7 +2553,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Triggers = newTriggers - log.Printf("PRE VARIABLES") + //log.Printf("PRE VARIABLES") for _, variable := range workflow.WorkflowVariables { if len(variable.Value) == 0 { log.Printf("Can't have an empty variable: %s", variable.Name) @@ -2601,7 +2601,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // FIXME - append all nodes (actions, triggers etc) to one single array here - log.Printf("PRE VARIABLES") + //log.Printf("PRE VARIABLES") if len(foundNodes) != len(allNodes) || len(workflow.Actions) <= 0 { // This shit takes a few seconds lol if !workflow.IsValid { @@ -2675,7 +2675,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // Check every app action and param to see whether they exist - log.Printf("PRE ACTIONS 2") + //log.Printf("PRE ACTIONS 2") newActions = []Action{} for _, action := range workflow.Actions { reservedApps := []string{ diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index bde4fff9..321d664f 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2221,7 +2221,7 @@ const AngularWorkflow = (props) => { "description": "Add your email provider", "trigger_type": "EMAIL", "errors": null, - "is_valid": true ? false : cloudSyncEnabled, + "is_valid": cloudSyncEnabled || isCloud ? true : false, "label": "Email", "environment": "cloud", "large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/AAAsIAGQAZAEBEQD/xAAeAAABAwUBAQAAAAAAAAAAAAAAAQgJAgQFBwoGA//EAEoQAAECBAMEBwMDEQgDAAAAAAECAwAEBREGBxIIITFRCRMiMkFSYRRicSNCQxUWGBk2U1dYc3WBlJWzwdLTJDNjZXKDkeEmgqH/2gAIAQEAAD8Ak8JKipSlBwuDSpSeDw8qeRguQQrUAQNAV4JH3s+sA7OnT8n1fcv9Bfzc7wWAAToIAOsJ8Uq++H0gI1XSUlYWdSkji6fMnkBAdS9SidesWUpI3OjyjkYXtXCgbEDSFW3JT5D6+sIOzp0/J9X3NX0F/NzvBYABOggA6wnxSr74fSAjVdJSVhZ1KSOLp8yeQEBJUSVKCysaVKHB0eVPIwXIIVqAIGgK8Ej72fWFS640kNtzjcukcGli6k/GENwVagAQO0EcEjmj1g33AATe1wD3SnmffgG/Tp337mv535T+EaB2k9tzInZilVyuMq+up4iUjWxh+mFLs8s23dbv0stnmsgnwBiNPOHpddozHExMSmWsrSsA0tZIaMs0JudCfV50FKf/AFQPjDYsT7S+0LjJ8zGJc68aTqiSrSqtPoQD6ISoJH6BGFkM5c3qW8Jim5qYvlXArUFNVuZSb89y43Tlv0ju15ltMtKZzUmsRSbZGuSxC0mebcHIrV8r/wALEP02c+l2ywzAmZXDGeND+saqvEIRVWXFP0x1Z3AOE9thPx1JHioQ/un1Kn1eQZqtLn2ZuSmkJdamZVwOIWlQuktKTuUg8xFybgnUACB2gjgkc0e9BvuAAm9rgHulPM+/CpDik3bal1p8FPd8/GEtp7Ojq+r36OPUe96wWv2dF79vR5/8T/qI/ukM6RMZMGcyWyUqDMzjZ5vRWKwmy26UlQ3IQOBmLHx3IFibncIeqvWKtiCqTVbrtSmahUJ11T0zNTLqnHXnFG5UpSiSSeZi0ggggh2GxRt8Y92X69K4cr83N1zLuadCZumLXrcp4Ue0/KXPZPiW+6r0O+JxsF4zwvmFhSl42wXV5eo0Sqy6ZySmWFakJbUO/wDHiCk7wQQd4jN2v2dF79vR5v8AE/6g6rrflPYfar/S69Or9EIAAE6QoAHshfFJ5r9Ibpt3bTrOzBkbPYhpb7f11V5aqZh9le8iZUm65i3i20ntcirQPGIA6nU6jWqlNVirzr05PTzy5iZmHllTjrqyVKWoneSSSSYtoIIIIIIkI6J/axmsA4+Rs84yqZ+tvFb5VQ1vL7EjVCNyN/Bt4C1vOEn5xiYndYghVr3IHeKuY9yKVJbJu63MrV4qY7h+EVA6rEKKwvclSuLp8quQiDnpWM5ZnMrafn8HS04pykZfy6KMw2D2BNEByZUPXWQj/aEM0h3uwvk5PYmoeLM4HcnqHmth/CU1KytdwrNy5M+uUdQtZmZBYIu83oN2z3wbcbWk7ym2Zej6zuwXJ49y5yXwbUqXNgpUPZlpelnh32XmyrU24k7ik7/0WMey+wI2OPxesJfqyv5oPsCNjj8XrCX6sr+aD7AjY4/F6wl+rq/mhs2b2SGzXjPGc3s9bKuzngiq4zZGjEWJ3pRTlKwkyrcVOKCrOzVr6GRex73AiIe65TvqRWqhSet632Kadl9enTq0LKb28L24R86ZUp6jVKUrFMmVy85IvtzMu8g2U24hQUlQPMEAx0h7O+abGdWR+DM0mnAF16lMuzRTxamgNDzQHIOJWI2IpxDZ0Lm3ZdQ4tti6U/CEdeDaHJhxYWAklahwdAF9KeRjmXzRxJMYxzLxXiyacUt2sVqdnlFRuflHlq/jHmIlv6D37is1T/mlM/cvQ5zNnZ7xtl/jWc2htlFUtIYrfs7iXCLy+rpeLGk7zcDczN2vpdFrnvcSTsrIPaHwNtBYcmKlhsv02t0h4ydfw7UE9XUKPOJJC2Xmzv3KBAWNyrbvEDaDjjbTa3XVpQhAKlKUbBIHEk+ENLxdnDj/AGr8T1LJ7Zgra6NgymPmSxhmU0LhB+kkaV4OPkGynu6gG4PAlwGUuT2AMjsDy2BMuqGin06XBcdWTrfm3j3333D2nHFHeVH/AOCwjmnxr92Ve/Oc1+9VGGibDogMVP1vZWmKI+4b4dxJOybS1G4S06ht7QPipxf/ADD4kurbGhE21LpHBtwXUn4xbVNpb9OnGAAFrl3EkI4JukgFHrHMFWpdyUrE/KvAhxmZdbUDxBCyDFnEuHQej/wjNU/5rTP3L0PQ2hs1cR4f+pOUWU/VTGZeOtbFK1jW3SZNO6YqkwPBtlJ7IPfcKUi++NdYh2H5LB1CoWLtnXFD2Fs1sLS6rV+ZUXG8SqWouPtVVP0yXnCo6+8gqFtwAGAbkdpzbFcTgXNLB1Qyay9pBEri1iXm9VQxPNo/vJeVdT/dyJ3XcG9YNgTvt6TFODKZsY4nlc2MsaEmRypn25em45oMi2eqpiUANsVllA8gsiYtvUiyzcpJhz8rOylSkGqjITTUzKzTKXmHmlhSHG1C6VJI3EEEEGOXnGv3ZV785zX71UYaJjehfk3mdn/GM4oHRM4sWEBfcsiUZ1Eeu+JBUhxSbttS60+Cnu+fjCAaDbR1fV9rRx6n3vW8c5+2Bl1MZV7TGYmDXmlIaZrkxNyhIsFy0wrr2lD00OJjT0SfdE5mphvJnIrOXHmJutdalatSmZSSlxqmKhNuNOpYlWU8VOOLISAOdzuBiQLZ5yrxJQTVs382Q0/mVjrQ9VAk6m6RJp3y9Llz4NtA9ojvuFSjfdG54N8fGekZOpyUxTajKtTMrNNLYfYdSFIdbULKSpJ3EEEgiG4ZXz07s0Zis7OuJpp1zAmJFvP5cVSYWSJVYut2hurPzkC62Ce83dHFFogGxr92Ve/Oc1+9VGGiezo0MupnLzY/wezPy5bm8RqmMROsqFiUvr+SWf8AaQ2besOl6rrflPYfar/S69Or9EIAAE6QoAHshfFJ5r9Ii76Y7Z4mJgUHaSw7IqWhlCKHiLQN6RcmWmP9Nypsn8mIizj3GWGdOY2T9Xka1gSuJlH6bO/VKWbflm5hlubDam0v9U4lSC4lClBKiLp1G1iY3v8AbSdtr8LTP7Ekf6UL9tJ22vwss/sOR/pQfbSdtr8LLP7Dkf6UH20nba/C0z+w5H+lHmMxOkB2qc1MOKwrjjMNmfkPaGZxrTSZRl1iYZWFtPNOobC21pULhSSDx8DDe5uamJ6aenZt1Tr8w4p11auKlqNyT8SY2ZszZH1raIzqw1ldSG1hqozSXKlMAHTKyLZCn3VHwsi4HvKSPGOjSj0im0CjyVBpMqJen06XalZZhG7Q22kJQE+4AAIulJbJu63MrV4qY7h+EVA6rEKKwvclSuLp8quQjB45wVhrMbB9YwNjGnIn6JW5VyQnWVjihYtoTysbEKHAgGOf7a72U8abKeZszhStMOzVAnVreoNXCfk5uXvuSojcHUAgLTz3jcRGi4IIIIIuqVSqnXKnK0ajSD89PzzyJeWlpdsrcecUbJQlI3kkkAAROd0eGxqjZiy7XiLF8sy5j/FjaFVEiyhJMDeiSB9D2lkbiqw4JEO58CrUQAdJV4pPkHu+sIpxDZ0Lm3ZdQ4tti6U/CFJKiVKUFle5Sk8HR5U8jBcghWoAgaArwSPIfe9Y8PnJkvl1nzgScy7zMoDVQpMyLtlXZekHfmvNucULHgR8DcEiIZtqro1s58gZucxFg6QmsbYJQVOonpFkqnJNrw9pYTcgAfSJuk8Tp4Qz9SSklKgQQbEHwgggjYeTOz9m7n/iFGHMq8Fz1YdCgJiZSjRKSiT8955XYQB6m58AYmN2LejtwJsyoYxri52XxVmC43unA3/ZpAEb0ygVvv4F09ojgEgm7wSSq5Kgsr3KUODo8qeRguQQrUAQNAV4JHkPvesKl1bY0Im2pdI4NuC6k/GENwVagAQO0EcEjmj1g33AATe1wD3SnmffgG/Tp337mv535T+EG4i4KiCbAnvE8j7kaHzf2Hdl/O1+YqONcraexU3jd6p0i8jNFfPU1ZLnxWlUNjxL0LOTs6+tzC+bWLKSkHUWpmXl5xKR4BJAbJjD0/oTcEoeH1Uz5rbzfe0sUZlolH+pTigFelo3Nlr0UuyZgSYZn6vQ6zjKaQQpr6uz3yBI462WQhNvRVxDscMYVwvgujMYfwfh+n0Wly/ZZlJCVQw2k8tCABp9YypsL6iQAe0U8Unkj3YDcE6gAQO0EcEjmj3oN9wAE3tcA90p5n34VIcUm7bUutPgp7vn4wOpS05MNtiyZdAW0PKo+MASkuIbIulbPXKHNfOEa+V9m6zf7Vq633rcIpSoqbQ6T2lvdQo80coVxRbRMLRuVLuBts+VJ4iKnEhtb6ECwl0BxseVR8YAlJcQ2RdK2euUOa+cI18r7N1m/wBq1db71uEUpUVNodJ7S3uoUeaOUK4otomFo3Kl3A22fKk8RFTiQ2t9CBYS6A42PKo+MASkuIbIulbPXKHNfOPtKSkvNy6JiYaC3F71KJO+P//Z', @@ -4990,50 +4990,59 @@ const AngularWorkflow = (props) => { } const outlookButton = - @@ -5044,20 +5053,25 @@ const AngularWorkflow = (props) => { if (triggerAuthentication.type === "outlook") { triggerInfo =
    -
    -
    -
    - Login: -
    -
    - {outlookButton} + {selectedTrigger.status === "running" ? null : + +
    +
    +
    + Login +
    +
    + {outlookButton} + + } - {triggerFolders === undefined || triggerFolders === null ? null : + {triggerFolders === undefined || triggerFolders === null ? + null :
    - Folders: (hold CTRL to select multiple) + Select a folder
    upload = ref} onChange={importFiles} /> + {workflows.length > 0 ? + + + + : null} + + + + + + const WorkflowView = () => { + if (workflows.length === 0) { + return ( +
    + +
    +

    Welcome to Shuffle

    +
    +
    +

    + Shuffle is a flexible, easy to use, automation platform allowing users to integrate their services and devices freely. It's made to significantly reduce the amount of manual labor, and is focused on security applications. Click here to learn more. +

    +
    +
    + If you want to jump straight into it, click here to create your first workflow: +
    +
    + + + + ..OR + + {workflowButtons} + +
    +
    +
    + ) + } + + return ( +
    +
    +
    +
    +

    Workflows

    +

    THIS IS SOME WORKFLOW INFORMATION WHY ISN’T IT GROWING THE CORRECT WAY.

    +
    +
    + {workflowButtons} +
    +
    + +
    +
    +
    +
    +
    +
    {workflows.length}
    +
    ACTIVE WORKFLOWS
    +
    +
    +
    +
    +
    +
    +
    +
    {workflows.length}
    +
    AVAILABE WORKFLOWS
    +
    +
    +
    +
    +
    +
    +
    +
    {workflows.length}
    +
    NOTIFICATIONS
    +
    +
    +
    +
    + +
    + {workflows.map((data, index) => { + return ( + + ) + })} +
    +
    +
    + ) + } + + const importWorkflowsFromUrl = (url) => { + console.log("IMPORT WORKFLOWS FROM ", downloadUrl) + + const parsedData = { + "url": url, + "field_3": downloadBranch || 'master' + } + + if (field1.length > 0) { + parsedData["field_1"] = field1 + } + + if (field2.length > 0) { + parsedData["field_2"] = field2 + } + + alert.success("Getting specific workflows from your URL.") + var cors = "cors" + fetch(globalUrl+"/api/v1/workflows/download_remote", { + method: "POST", + mode: "cors", + headers: { + 'Accept': 'application/json', + }, + body: JSON.stringify(parsedData), + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + alert.success("Successfully loaded workflows from "+downloadUrl) + getAvailableWorkflows() + } + + return response.json() + }) + .then((responseJson) => { + console.log("DATA: ", responseJson) + if (!responseJson.success) { + if (responseJson.reason !== undefined) { + alert.error("Failed loading: "+responseJson.reason) + } else { + alert.error("Failed loading") + } + } + }) + .catch(error => { + alert.error(error.toString()) + }) + } + + const handleGithubValidation = () => { + importWorkflowsFromUrl(downloadUrl) + setLoadWorkflowsModalOpen(false) + } + + const workflowDownloadModalOpen = loadWorkflowsModalOpen ? + { + }} + PaperProps={{ + style: { + backgroundColor: surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + +
    + Load workflows from github repo +
    + + + +
    +
    +
    + + Repository (supported: github, gitlab, bitbucket) + 0 ? userdata.active_org.defaults.workflow_download_repo : downloadUrl} + InputProps={{ + style:{ + color: "white", + height: "50px", + fontSize: "1em", + }, + }} + onChange={e => setDownloadUrl(e.target.value)} + placeholder="https://github.com/frikky/shuffle-apps" + fullWidth + /> + + Branch (default value is "master"): +
    + 0 ? userdata.active_org.defaults.workflow_download_branch : downloadBranch} + InputProps={{ + style:{ + color: "white", + height: "50px", + fontSize: "1em", + }, + }} + onChange={e => setDownloadBranch(e.target.value)} + placeholder="master" + fullWidth + /> +
    + + Authentication (optional - private repos etc): +
    + setField1(e.target.value)} + type="username" + placeholder="Username / APIkey (optional)" + fullWidth + /> + setField2(e.target.value)} + type="password" + placeholder="Password (optional)" + fullWidth + /> +
    +
    + + + + +
    + : null + + const loadedCheck = isLoaded && isLoggedIn && workflowDone ? +
    + + {modalView} + {deleteModal} + {workflowDownloadModalOpen} +
    + : +
    + + + Loading Workflows + +
    + + + // Maybe use gridview or something, idk + return ( +
    + {loadedCheck} +
    + ) +} + +export default MyView From 49564cdc445b2cc8d8ba04dbe3b2118ab4d66aa0 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 16 Mar 2021 01:39:47 +0100 Subject: [PATCH 169/185] Updated compose to 0.8.64 --- backend/Dockerfile | 1 + backend/go-app/main.go | 14 +-- backend/go-app/oauth2.go | 121 +++++++++++++++++++++++++ backend/go-app/walkoff.go | 64 +++++++++---- docker-compose.yml | 4 +- frontend/src/views/AngularWorkflow.jsx | 4 +- 6 files changed, 178 insertions(+), 30 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 40bb50ea..ea93625e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,6 +9,7 @@ ADD ./go-app/walkoff.go /app ADD ./go-app/docker.go /app ADD ./go-app/codegen.go /app ADD ./go-app/files.go /app +ADD ./go-app/oauth2.go /app ADD ./go-app/go.mod /app diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 737e8b0e..0d855263 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -5882,19 +5882,19 @@ func handleCloudJob(job CloudSyncJob) error { log.Printf("[INFO] Should handle outlook webhook for workflow %s with start node %s and data of length %d", job.PrimaryItemId, job.SecondaryItem, len(job.ThirdItem)) err = handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "outlook", string(emailBytes)) if err != nil { - log.Printf("Failed executing workflow from cloud outlook hook: %s", err) + log.Printf("[WARNING] Failed executing workflow from cloud outlook hook: %s", err) } else { - log.Printf("Successfully executed workflow from cloud outlook hook!") + log.Printf("[INFO] Successfully executed workflow from cloud outlook hook!") } } } else if job.Type == "webhook" { if job.Action == "execute" { - log.Printf("Should handle normal webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + log.Printf("[INFO] Should handle normal webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "webhook", job.ThirdItem) if err != nil { - log.Printf("Failed executing workflow from cloud hook: %s", err) + log.Printf("[INFO] Failed executing workflow from cloud hook: %s", err) } else { - log.Printf("Successfully executed workflow from cloud hook!") + log.Printf("[INFO] Successfully executed workflow from cloud hook!") } } @@ -5903,9 +5903,9 @@ func handleCloudJob(job CloudSyncJob) error { log.Printf("Should handle schedule for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "schedule", job.ThirdItem) if err != nil { - log.Printf("Failed executing workflow from cloud schedule: %s", err) + log.Printf("[INFO] Failed executing workflow from cloud schedule: %s", err) } else { - log.Printf("Successfully executed workflow from cloud schedule") + log.Printf("[INFO] Successfully executed workflow from cloud schedule") } } } else if job.Type == "email_trigger" { diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go index 8c4a355f..b00f6c7a 100644 --- a/backend/go-app/oauth2.go +++ b/backend/go-app/oauth2.go @@ -17,6 +17,41 @@ import ( "golang.org/x/oauth2" ) +// This is what the structure should be when it's sent into a workflow +type ParsedShuffleMail struct { + Body struct { + URI []string `json:"uri"` + Email []string `json:"email"` + Domain []string `json:"domain"` + ContentHeader struct { + } `json:"content_header"` + Content string `json:"content"` + ContentType string `json:"content_type"` + Hash string `json:"hash"` + RawBody string `json:"raw_body"` + } `json:"body"` + Header struct { + Subject string `json:"subject"` + From string `json:"from"` + To []string `json:"to"` + Date string `json:"date"` + Received []struct { + Src string `json:"src"` + From []string `json:"from"` + By []string `json:"by"` + With string `json:"with"` + Date string `json:"date"` + } `json:"received"` + ReceivedDomain []string `json:"received_domain"` + ReceivedIP []string `json:"received_ip"` + Header struct { + } `json:"header"` + } `json:"header"` + MessageID string `json:"message_id"` + EmailFileid string `json:"email_fileid"` + AttachmentUids []string `json:"attachment_uids"` +} + type FullEmail struct { OdataContext string `json:"@odata.context"` OdataEtag string `json:"@odata.etag"` @@ -69,6 +104,19 @@ type FullEmail struct { Flag struct { Flagstatus string `json:"flagStatus"` } `json:"flag"` + Attachments []struct { + OdataType string `json:"@odata.type"` + OdataMediacontenttype string `json:"@odata.mediaContentType"` + ID string `json:"id"` + Lastmodifieddatetime time.Time `json:"lastModifiedDateTime"` + Name string `json:"name"` + Contenttype string `json:"contentType"` + Size int `json:"size"` + Isinline bool `json:"isInline"` + Contentid interface{} `json:"contentId"` + Contentlocation interface{} `json:"contentLocation"` + Contentbytes string `json:"contentBytes"` + } } type MailData struct { @@ -118,6 +166,47 @@ type OutlookFolders struct { Value []OutlookFolder `json:"value"` } +func getOutlookAttachment(client *http.Client, emailId, attachmentId string) ([]FullEmail, error) { + //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") + + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s/attachments/%s", emailId, attachmentId) + //log.Printf("Outlook email URL: %#v", requestUrl) + + ret, err := client.Get(requestUrl) + if err != nil { + log.Printf("[INFO] OutlookErr: %s", err) + return []FullEmail{}, err + } + + body, err := ioutil.ReadAll(ret.Body) + if err != nil { + log.Printf("[WARNING] Failed body decoding from outlook email") + return []FullEmail{}, err + } + + //type FullEmail struct { + log.Printf("[INFO] Attachment Body: %s", string(body)) + log.Printf("[INFO] Status email: %d", ret.StatusCode) + if ret.StatusCode != 200 { + return []FullEmail{}, err + } + + //log.Printf("Body: %s", string(body)) + + /* + parsedmail := FullEmail{} + err = json.Unmarshal(body, &parsedmail) + if err != nil { + log.Printf("[INFO] Email unmarshal error: %s", err) + return []FullEmail{}, err + } + + emails = append(emails, parsedmail) + */ + + return []FullEmail{}, nil +} + func getOutlookEmail(client *http.Client, maildata MailData) ([]FullEmail, error) { //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") @@ -991,6 +1080,38 @@ func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) { email = emails[0] } + // Parse indicators (domains, emails, ips, domains etc)! + newEmail := ParsedShuffleMail{} + newEmail.Body.ContentType = email.Body.Contenttype + newEmail.Body.Content = email.Body.Content + newEmail.Body.RawBody = email.Body.Content + + newEmail.Header.Subject = email.Subject + newEmail.Header.From = email.From.Emailaddress.Address + for _, to := range email.Torecipients { + newEmail.Header.To = append(newEmail.Header.To, to.Emailaddress.Address) + } + newEmail.Header.Date = email.Receiveddatetime.String() + + newEmail.MessageID = email.ID + + if email.Hasattachments { + log.Printf("SHOULD HANDLE ATTACHMENTS FOR EMAIL!") + + for _, attachment := range email.Attachments { + parsedAttachment, err := getOutlookAttachment(outlookClient, email.ID, attachment.ID) + if err != nil { + log.Printf("Failed attachment %s: %s", attachment.ID, err) + continue + } + + log.Printf("ATTACHMENT: %#v", parsedAttachment) + } + //log.Printf("%#v", attachments) + //log.Printf("%s", err) + //GET /users/{id | userPrincipalName}/events/{id}/attachments/{id} + } + emailBytes, err := json.Marshal(email) if err != nil { log.Printf("[INFO] Failed email marshaling: %s", err) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 16e1a653..7a20bec6 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2565,6 +2565,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // Check every app action and param to see whether they exist //log.Printf("PRE ACTIONS 2") + allAuths, autherr := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) newActions = []Action{} for _, action := range workflow.Actions { reservedApps := []string{ @@ -2655,34 +2656,60 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // Check to see if the action is valid if curappaction.Name != action.Name { log.Printf("[ERROR] Action %s in app %s doesn't exist.", action.Name, curapp.Name) - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Action %s in app %s doesn't exist"}`, action.Name, curapp.Name))) - return - } + thisError := fmt.Sprintf("%s: Action %s in app %s doesn't exist", action.Label, action.Name, action.AppName) + workflow.Errors = append(workflow.Errors, thisError) + workflow.IsValid = false + action.Errors = append(action.Errors, thisError) + action.IsValid = false + //if workflow.PreviouslySaved { + // resp.WriteHeader(401) + // resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Action %s in app %s doesn't exist"}`, action.Name, curapp.Name))) + // return + //} } // FIXME - check all parameters to see if they're valid // Includes checking required fields + selectedAuth := AppAuthenticationStorage{} + if len(action.AuthenticationId) > 0 && autherr == nil { + for _, auth := range allAuths { + if auth.Id == action.AuthenticationId { + selectedAuth = auth + break + } + } + } + newParams := []WorkflowAppActionParameter{} for _, param := range curappaction.Parameters { - found := false + paramFound := false // Handles check for parameter exists + value not empty in used fields for _, actionParam := range action.Parameters { if actionParam.Name == param.Name { - found = true + paramFound = true if actionParam.Value == "" && actionParam.Variant == "STATIC_VALUE" && actionParam.Required == true { - log.Printf("[WARNING] Appaction %s with required param '%s' is empty. Can't save.", action.Name, param.Name) - //if workflow.PreviouslySaved { - // resp.WriteHeader(401) - // resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Appaction %s in app %s with required param '%s' is empty.", "node_id": "%s"}`, action.Name, action.AppName, param.Name, action.ID))) - // return - //} else { + // Validating if the field is an authentication field + if len(selectedAuth.Id) > 0 { + authFound := false + for _, field := range selectedAuth.Fields { + if field.Key == actionParam.Name { + authFound = true + //log.Printf("FOUND REQUIRED KEY %s IN AUTH", field.Key) + break + } + } - thisError := fmt.Sprintf("Missing parameter %s", param.Name) + if authFound { + newParams = append(newParams, actionParam) + continue + } + } + + log.Printf("[WARNING] Appaction %s with required param '%s' is empty. Can't save.", action.Name, param.Name) + thisError := fmt.Sprintf("%s is missing reqired parameter %s", action.Label, param.Name) action.Errors = append(action.Errors, thisError) workflow.Errors = append(workflow.Errors, thisError) action.IsValid = false @@ -2698,7 +2725,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // Handles check for required params - if !found && param.Required { + if !paramFound && param.Required { log.Printf("Appaction %s with required param %s doesn't exist.", action.Name, param.Name) thisError := fmt.Sprintf("Parameter %s is required", param.Name) action.Errors = append(action.Errors, thisError) @@ -2719,13 +2746,10 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } - //log.Printf("PRE SAVECHECK") if !workflow.PreviouslySaved { log.Printf("[WORKFLOW INIT] NOT PREVIOUSLY SAVED - SET ACTION AUTH!") - //AuthenticationId string `json:"authentication_id,omitempty" datastore:"authentication_id"` - allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - if err == nil && len(workflowapps) > 0 && apperr == nil { + if autherr == nil && len(workflowapps) > 0 && apperr == nil { //log.Printf("Setting actions") actionFixing := []Action{} appsAdded := []string{} @@ -5389,7 +5413,7 @@ func getAppAuthentication(resp http.ResponseWriter, request *http.Request) { return } - // Cleanup for frontend + // Cleanup for frontend usage. User shouldn't be able to get the data. newAuth := []AppAuthenticationStorage{} for _, auth := range allAuths { newAuthField := auth diff --git a/docker-compose.yml b/docker-compose.yml index b26f3c1a..b96b2441 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.63 + image: ghcr.io/frikky/shuffle-frontend:0.8.64 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.63 + image: ghcr.io/frikky/shuffle-backend:0.8.64 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 9bb3ae21..40f9b948 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -204,8 +204,10 @@ const AngularWorkflow = (props) => { }) const [elements, setElements] = useState([]) + // No point going as fast, as the nodes aren't realtime anymore, but bulk updated. + // Set it from 2500 to 6000 to reduce overall load const { start, stop } = useInterval({ - duration: 2500, + duration: 6000, startImmediate: false, callback: () => { fetchUpdates() From 8148eb924c9a73c0cbf512c0639a4f3554e17cf4 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 20 Mar 2021 16:16:53 +0100 Subject: [PATCH 170/185] Moved worker and orborus to run with shuffle-shared --- backend/app_sdk/app_base.py | 4 +- backend/go-app/main.go | 2 +- backend/go-app/walkoff.go | 14 +- frontend/src/defaultCytoscapeStyle.js | 9 +- functions/onprem/orborus/Dockerfile | 3 +- functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/go.mod | 2 +- functions/onprem/orborus/go.sum | 383 ++++++++++++++++++++++++++ functions/onprem/orborus/orborus.go | 21 +- functions/onprem/worker/Dockerfile | 3 +- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 262 +++++++++--------- 12 files changed, 553 insertions(+), 154 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 4db578cb..c18be280 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1208,7 +1208,9 @@ class AppBase: outercnt += 1 except KeyError as e: - print("Lower keyerror: %s" % e) + print("[INFO] Lower keyerror: %s" % e) + return "", False + #return basejson #return "KeyError: Couldn't find key: %s" % e diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 0d855263..abb13c8b 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3695,7 +3695,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] Failed to increase total workflows: %s", err) } - log.Printf("Set up a new hook with ID %s and environment %s", newId, hook.Environment) + log.Printf("[INFO] Set up a new hook with ID %s and environment %s", newId, hook.Environment) resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 7a20bec6..456ecb4f 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1158,6 +1158,13 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl resultLength := len(workflowExecution.Results) dbSave := false setExecution := true + + if actionResult.Action.ID == "" { + //log.Printf("[ERROR] Failed handling EMPTY action %#v", actionResult) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't handle empty action"}`))) + return + } //tx, err := dbclient.NewTransaction(ctx) //if err != nil { // log.Printf("client.NewTransaction: %v", err) @@ -1309,6 +1316,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // FIXME rebuild to be like this or something // workflowExecution/ExecutionId/Nodes/NodeId // Find the appropriate action + //log.Printf("[INFO] Setting value of %s in workflow %s to %s (1)", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status) if len(workflowExecution.Results) > 0 { // FIXME skip := false @@ -1344,14 +1352,14 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } } - log.Printf("[INFO] Updating %s in workflow %s from %s to %s", actionResult.Action.ID, workflowExecution.ExecutionId, workflowExecution.Results[outerindex].Status, actionResult.Status) + log.Printf("[INFO] Updating %s in workflow %s from %s to %s (3)", actionResult.Action.ID, workflowExecution.ExecutionId, workflowExecution.Results[outerindex].Status, actionResult.Status) workflowExecution.Results[outerindex] = actionResult } else { - log.Printf("[INFO] Setting value of %s in workflow %s to %s", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status) + log.Printf("[INFO] Setting value of %s in workflow %s to %s (1)", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status) workflowExecution.Results = append(workflowExecution.Results, actionResult) } } else { - log.Printf("[INFO] Setting value of %s in workflow %s to %s", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status) + log.Printf("[INFO] Setting value of %s in workflow %s to %s (2)", actionResult.Action.ID, workflowExecution.ExecutionId, actionResult.Status) workflowExecution.Results = append(workflowExecution.Results, actionResult) } diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index ab8fae1e..90c7aae8 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -6,7 +6,7 @@ const data = [{ 'font-family': 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif', 'font-weight': 'lighter', 'margin-right': '10px', - 'font-size': '15px', + 'font-size': '18px', 'width': '80px', 'height': '80px', 'color': 'white', @@ -20,14 +20,15 @@ const data = [{ selector: 'edge', css: { 'target-arrow-shape': 'triangle', - 'target-arrow-color': 'yellow', + 'target-arrow-color': 'grey', 'curve-style': 'unbundled-bezier', 'label': 'data(label)', 'text-margin-y': '-15px', + 'width': '2px', "color": "white", "line-fill": "linear-gradient", - "line-gradient-stop-colors": ["cyan", "yellow"], "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": ["grey", "grey"], }, }, { @@ -183,7 +184,7 @@ const data = [{ css: { 'background-color': "#f85a3e", 'border-color': '#f85a3e', - 'border-width': '8px', + 'border-width': '12px', 'transition-property': 'border-width', 'transition-duration': '0.25s', }, diff --git a/functions/onprem/orborus/Dockerfile b/functions/onprem/orborus/Dockerfile index 7c178fd8..6479c09e 100644 --- a/functions/onprem/orborus/Dockerfile +++ b/functions/onprem/orborus/Dockerfile @@ -10,7 +10,8 @@ RUN go get github.com/docker/docker/api/types && \ go get github.com/docker/docker/client && \ go get github.com/mackerelio/go-osstat/cpu && \ go get github.com/mackerelio/go-osstat/memory && \ - go get github.com/satori/go.uuid + go get github.com/satori/go.uuid && \ + go get github.com/frikky/shuffle-shared RUN go build RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o orborus . diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 1555b51f..2ea4d555 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.63 +VERSION=0.8.64 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 4e17a529..f7d50e57 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -9,6 +9,7 @@ require ( github.com/docker/docker v20.10.1+incompatible github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/frikky/shuffle-shared v0.0.12 // indirect github.com/gogo/protobuf v1.3.1 // indirect github.com/mackerelio/go-osstat v0.1.0 github.com/opencontainers/go-digest v1.0.0 // indirect @@ -16,5 +17,4 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.7.0 // indirect - google.golang.org/grpc v1.34.0 // indirect ) diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index e8c720de..d3ea7e29 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -1,9 +1,54 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.4.0 h1:CFDJm15RpYXeEblQ0TMDUrYtqmBmbAWTy536nA8JIc8= +cloud.google.com/go/datastore v1.4.0/go.mod h1:d18825/a9bICdAIJy2EkHs9joU4RlIZ1t6l8WDdbdY0= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.12.0 h1:4y3gHptW1EHVtcPAVE0eBBlFuGqEejTTG3KdIE0lUX4= +cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= @@ -19,14 +64,42 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frikky/kin-openapi v0.38.0 h1:V7ttwIJS8Vks4KL+mZVj1ZSqhIcQtgaG8akeqXEQgsE= +github.com/frikky/kin-openapi v0.38.0/go.mod h1:Fr28TtCHL4K0kIqtqui8HWxN1LG5uAh3z/tDfFyiA1s= +github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu2YUSjhw= +github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -35,17 +108,56 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mackerelio/go-osstat v0.1.0 h1:e57QHeHob8kKJ5FhcXGdzx5O6Ktuc5RHMDIkeqhgkFA= github.com/mackerelio/go-osstat v0.1.0/go.mod h1:1K3NeYLhMHPvzUu+ePYXtoB58wkaRpxZsGClZBJyIFw= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= @@ -54,6 +166,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -62,47 +175,301 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423 h1:/hEknzWkMPCjTo7StMHRrBRa8YBbXuBWfck8680k3RE= +golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190410235845-0ad05ae3009d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963 h1:K+NlvTLy0oONtRtkl1jRD9xIhnItbG2PiE7YOdjPb+k= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo= +google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0 h1:l2Nfbl2GPXdWorv+dT2XfinX2jOOw4zv1VhLstx+6rE= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595 h1:x7nk+/4+SvuTDI4wnzQUlhvi+DTpyfncXBo3QWTFs7U= +google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.34.1 h1:ugq+9++ZQPFzM2pKUMCIK8gj9M0pFyuUWO9Q8kwEDQw= +google.golang.org/grpc v1.34.1/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -111,9 +478,25 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 015d2986..ad508e8a 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -5,6 +5,8 @@ package main */ import ( + "github.com/frikky/shuffle-shared" + "bytes" "context" "encoding/json" @@ -55,19 +57,6 @@ var runningMode = strings.ToLower(os.Getenv("RUNNING_MODE")) var cleanupEnv = strings.ToLower(os.Getenv("CLEANUP")) var executionIds = []string{} -type ExecutionRequestWrapper struct { - Data []ExecutionRequest `json:"data"` -} - -type ExecutionRequest struct { - ExecutionId string `json:"execution_id"` - ExecutionArgument string `json:"execution_argument"` - WorkflowId string `json:"workflow_id"` - Authorization string `json:"authorization"` - Status string `json:"status"` - Type string `json:"type"` -} - var dockercli *dockerclient.Client var containerId string @@ -259,7 +248,7 @@ func initializeImages() { log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.8.62" + workerVersion = "0.8.64" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } @@ -477,7 +466,7 @@ func main() { continue } - var executionRequests ExecutionRequestWrapper + var executionRequests shuffle.ExecutionRequestWrapper err = json.Unmarshal(body, &executionRequests) if err != nil { log.Printf("[WARNING] Failed executionrequest in queue unmarshaling: %s", err) @@ -523,7 +512,7 @@ func main() { } // New, abortable version. Should check executionid and remove everything else - var toBeRemoved ExecutionRequestWrapper + var toBeRemoved shuffle.ExecutionRequestWrapper for _, execution := range executionRequests.Data { if len(execution.ExecutionArgument) > 0 { log.Printf("[INFO] Argument: %#v", execution.ExecutionArgument) diff --git a/functions/onprem/worker/Dockerfile b/functions/onprem/worker/Dockerfile index a9df5c2b..62aa2d0c 100644 --- a/functions/onprem/worker/Dockerfile +++ b/functions/onprem/worker/Dockerfile @@ -10,7 +10,8 @@ RUN go get github.com/docker/docker/api/types && \ go get github.com/docker/docker/api/types/container && \ go get github.com/docker/docker/client && \ go get github.com/gorilla/mux && \ - go get github.com/patrickmn/go-cache + go get github.com/patrickmn/go-cache && \ + go get github.com/frikky/shuffle-shared RUN go build RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker . diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 44d134c5..424b6207 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-worker -VERSION=0.8.63 +VERSION=0.8.64 echo "Running docker build with $NAME:$VERSION" #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 0594b142..9966785a 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -1,6 +1,8 @@ package main import ( + "github.com/frikky/shuffle-shared" + "bytes" "context" "encoding/json" @@ -83,6 +85,7 @@ func init() { } } +/* type Userapi struct { Username string `datastore:"username"` ApiKey string `datastore:"apikey"` @@ -90,12 +93,12 @@ type Userapi struct { type ExecutionInfo struct { TotalApiUsage int64 `json:"total_api_usage" datastore:"total_api_usage"` - TotalWorkflowExecutions int64 `json:"total_workflow_executions" datastore:"total_workflow_executions"` + Totalshuffle.WorkflowExecutions int64 `json:"total_workflow_executions" datastore:"total_workflow_executions"` TotalAppExecutions int64 `json:"total_app_executions" datastore:"total_app_executions"` TotalCloudExecutions int64 `json:"total_cloud_executions" datastore:"total_cloud_executions"` TotalOnpremExecutions int64 `json:"total_onprem_executions" datastore:"total_onprem_executions"` DailyApiUsage int64 `json:"daily_api_usage" datastore:"daily_api_usage"` - DailyWorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` + Dailyshuffle.WorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` DailyAppExecutions int64 `json:"daily_app_executions" datastore:"daily_app_executions"` DailyCloudExecutions int64 `json:"daily_cloud_executions" datastore:"daily_cloud_executions"` DailyOnpremExecutions int64 `json:"daily_onprem_executions" datastore:"daily_onprem_executions"` @@ -112,6 +115,7 @@ type StatisticsItem struct { Fieldname string `json:"field_name" datastore:"field_name"` Data []StatisticsData `json:"data" datastore:"data"` } +*/ // "Execution by status" // Execution history @@ -130,6 +134,7 @@ type StatisticsItem struct { // Baseline map[string]int64 `json:"baseline" datastore:"baseline"` //} +/* type ParsedOpenApi struct { Body string `datastore:"body,noindex" json:"body"` ID string `datastore:"id" json:"id"` @@ -139,7 +144,7 @@ type ParsedOpenApi struct { // Limits set for a user so that they can't do a shitload type UserLimits struct { DailyApiUsage int64 `json:"daily_api_usage" datastore:"daily_api_usage"` - DailyWorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` + Dailyshuffle.WorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` DailyCloudExecutions int64 `json:"daily_cloud_executions" datastore:"daily_cloud_executions"` DailyTriggers int64 `json:"daily_triggers" datastore:"daily_triggers"` DailyMailUsage int64 `json:"daily_mail_usage" datastore:"daily_mail_usage"` @@ -380,7 +385,7 @@ type Info struct { Description string `json:"description" datastore:"description,noindex"` } -// Actions to be done by webhooks etc +// shuffle.Actions to be done by webhooks etc // Field is the actual field to use from json type HookAction struct { Type string `json:"type" datastore:"type"` @@ -425,7 +430,7 @@ type SyncFeatures struct { Notifications SyncData `json:"notifications" datastore:"notifications"` EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` AppExecutions SyncData `json:"app_executions" datastore:"app_executions"` - WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` + shuffle.WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` Apps SyncData `json:"apps" datastore:"apps"` Workflows SyncData `json:"workflows" datastore:"workflows"` Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` @@ -520,7 +525,7 @@ type WorkflowApp struct { LastRuntime int64 `json:"last_runtime" datastore:"last_runtime"` } -type WorkflowAppActionParameter struct { +type shuffle.WorkflowAppActionParameter struct { Description string `json:"description" datastore:"description,noindex" yaml:"description"` ID string `json:"id" datastore:"id" yaml:"id,omitempty"` Name string `json:"name" datastore:"name" yaml:"name"` @@ -561,7 +566,7 @@ type WorkflowAppAction struct { Tags []string `json:"tags" datastore:"tags" yaml:"tags"` Authentication []AuthenticationStore `json:"authentication" datastore:"authentication,noindex" yaml:"authentication,omitempty"` Tested bool `json:"tested" datastore:"tested" yaml:"tested"` - Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"` + Parameters []shuffle.WorkflowAppActionParameter `json:"parameters" datastore: "parameters"` ExecutionVariable struct { Description string `json:"description" datastore:"description,noindex"` ID string `json:"id" datastore:"id"` @@ -579,7 +584,7 @@ type WorkflowAppAction struct { AuthNotRequired bool `json:"auth_not_required" datastore:"auth_not_required" yaml:"auth_not_required"` } -type WorkflowExecution struct { +type shuffle.WorkflowExecution struct { Type string `json:"type" datastore:"type"` Status string `json:"status" datastore:"status"` Start string `json:"start" datastore:"start"` @@ -607,7 +612,7 @@ type WorkflowExecution struct { OrgId string `json:"org_id" datastore:"org_id"` } -type Action struct { +type shuffle.Action struct { AppName string `json:"app_name,omitempty" datastore:"app_name"` AppVersion string `json:"app_version,omitempty" datastore:"app_version"` AppID string `json:"app_id,omitempty" datastore:"app_id"` @@ -622,7 +627,7 @@ type Action struct { LargeImage string `json:"large_image,omitempty" datastore:"large_image,noindex" yaml:"large_image" required:false` Environment string `json:"environment,omitempty" datastore:"environment"` Name string `json:"name,omitempty" datastore:"name"` - Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` + Parameters []shuffle.WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` ExecutionVariable struct { Description string `json:"description,omitempty" datastore:"description,noindex"` ID string `json:"id,omitempty" datastore:"id"` @@ -640,7 +645,7 @@ type Action struct { } // Added environment for location to execute -type Trigger struct { +type shuffle.Trigger struct { AppName string `json:"app_name" datastore:"app_name"` Description string `json:"description" datastore:"description,noindex"` LongDescription string `json:"long_description" datastore:"long_description"` @@ -657,7 +662,7 @@ type Trigger struct { TriggerType string `json:"trigger_type" datastore:"trigger_type"` Name string `json:"name" datastore:"name"` Tags []string `json:"tags" datastore:"tags" yaml:"tags"` - Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` + Parameters []shuffle.WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` Position struct { X float64 `json:"x" datastore:"x"` Y float64 `json:"y" datastore:"y"` @@ -676,9 +681,9 @@ type Branch struct { // Same format for a lot of stuff type Condition struct { - Condition WorkflowAppActionParameter `json:"condition" datastore:"condition"` - Source WorkflowAppActionParameter `json:"source" datastore:"source"` - Destination WorkflowAppActionParameter `json:"destination" datastore:"destination"` + Condition shuffle.WorkflowAppActionParameter `json:"condition" datastore:"condition"` + Source shuffle.WorkflowAppActionParameter `json:"source" datastore:"source"` + Destination shuffle.WorkflowAppActionParameter `json:"destination" datastore:"destination"` } type Schedule struct { @@ -729,8 +734,8 @@ type Workflow struct { ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` } -type ActionResult struct { - Action Action `json:"action" datastore:"action,noindex"` +type shuffle.ActionResult struct { + Action shuffle.Action `json:"action" datastore:"action,noindex"` ExecutionId string `json:"execution_id" datastore:"execution_id"` Authorization string `json:"authorization" datastore:"authorization"` Result string `json:"result" datastore:"result,noindex"` @@ -775,9 +780,10 @@ type AppExecutionExample struct { SuccessExamples []string `json:"success_examples" datastore:"success_examples,noindex"` FailureExamples []string `json:"failure_examples" datastore:"failure_examples,noindex"` } +*/ // removes every container except itself (worker) -func shutdown(workflowExecution WorkflowExecution, nodeId string, reason string, handleResultSend bool) { +func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason string, handleResultSend bool) { log.Printf("[INFO] Shutdown (%s) started with reason %s", workflowExecution.Status, reason) //reason := "Error in execution" @@ -881,7 +887,7 @@ func shutdown(workflowExecution WorkflowExecution, nodeId string, reason string, } // Deploys the internal worker whenever something happens -func deployApp(cli *dockerclient.Client, image string, identifier string, env []string) error { +func deployApp(cli *dockerclient.Client, image string, identifier string, env []string, workflowExecution shuffle.WorkflowExecution) error { // form basic hostConfig ctx := context.Background() hostConfig := &container.HostConfig{ @@ -968,47 +974,51 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] log.Printf("[INFO] Container %s was created for %s", cont.ID, identifier) // Waiting to see if it exits.. Stupid, but stable(r) - time.Sleep(2 * time.Second) + if workflowExecution.ExecutionSource != "default" { + log.Printf("Handling NON-default execution source %s - NOT waiting and validating!", workflowExecution.ExecutionSource) + } else if workflowExecution.ExecutionSource == "default" { + time.Sleep(2 * time.Second) - stats, err := cli.ContainerInspect(ctx, cont.ID) - if err != nil { - log.Printf("[ERROR] Failed getting container stats") - } else { - //log.Printf("[INFO] Info for container: %#v", stats) - //log.Printf("%#v", stats.Config) - //log.Printf("%#v", stats.ContainerJSONBase.State) - log.Printf("[INFO] EXECUTION STATUS: %s", stats.ContainerJSONBase.State.Status) - if stats.ContainerJSONBase.State.Status == "exited" { - logOptions := types.ContainerLogsOptions{ - ShowStdout: true, + stats, err := cli.ContainerInspect(ctx, cont.ID) + if err != nil { + log.Printf("[ERROR] Failed getting container stats") + } else { + //log.Printf("[INFO] Info for container: %#v", stats) + //log.Printf("%#v", stats.Config) + //log.Printf("%#v", stats.ContainerJSONBase.State) + log.Printf("[INFO] EXECUTION STATUS: %s", stats.ContainerJSONBase.State.Status) + if stats.ContainerJSONBase.State.Status == "exited" { + logOptions := types.ContainerLogsOptions{ + ShowStdout: true, + } + + out, err := cli.ContainerLogs(ctx, cont.ID, logOptions) + if err != nil { + log.Printf("[INFO] Failed getting logs: %s", err) + } else { + log.Printf("IN ELSE FOR DEPLOY") + buf := new(strings.Builder) + io.Copy(buf, out) + logs := buf.String() + log.Printf("Logs: %s", logs) + + //log.Printf(logs) + // check errors + /* + if strings.Contains(logs, "Error") { + log.Printf("ERROR IN %s?", cont.ID) + log.Println(logs) + //return errors.New(fmt.Sprintf("ERROR FROM CONTAINER %s", cont.ID)) + } else { + log.Printf("NORMAL EXEC OF %s?", cont.ID) + } + */ + } + + log.Printf("ERROR IN CONTAINER DEPLOYMENT - ITS EXITED!") + + return errors.New(fmt.Sprintf(`{"success": false, "reason": "Container %s exited prematurely.","debug": "docker logs -f %s"}`, cont.ID, cont.ID)) } - - out, err := cli.ContainerLogs(ctx, cont.ID, logOptions) - if err != nil { - log.Printf("[INFO] Failed getting logs: %s", err) - } else { - log.Printf("IN ELSE FOR DEPLOY") - buf := new(strings.Builder) - io.Copy(buf, out) - logs := buf.String() - log.Printf("Logs: %s", logs) - - //log.Printf(logs) - // check errors - /* - if strings.Contains(logs, "Error") { - log.Printf("ERROR IN %s?", cont.ID) - log.Println(logs) - //return errors.New(fmt.Sprintf("ERROR FROM CONTAINER %s", cont.ID)) - } else { - log.Printf("NORMAL EXEC OF %s?", cont.ID) - } - */ - } - - log.Printf("ERROR IN CONTAINER DEPLOYMENT - ITS EXITED!") - - return errors.New(fmt.Sprintf(`{"success": false, "reason": "Container %s exited prematurely.","debug": "docker logs -f %s"}`, cont.ID, cont.ID)) } } @@ -1072,7 +1082,7 @@ func removeContainer(containername string) error { return nil } -func runFilter(workflowExecution WorkflowExecution, action Action) { +func runFilter(workflowExecution shuffle.WorkflowExecution, action shuffle.Action) { // 1. Get the parameter $.#.id if action.Label == "filter_cases" && len(action.Parameters) > 0 { if action.Parameters[0].Variant == "ACTION_RESULT" { @@ -1088,7 +1098,7 @@ func runFilter(workflowExecution WorkflowExecution, action Action) { } -func handleSubworkflowExecution(client *http.Client, workflowExecution WorkflowExecution, action Trigger, baseAction Action) error { +func handleSubworkflowExecution(client *http.Client, workflowExecution shuffle.WorkflowExecution, action shuffle.Trigger, baseAction shuffle.Action) error { apikey := "" workflowId := "" executionArgument := "" @@ -1140,14 +1150,14 @@ func handleSubworkflowExecution(client *http.Client, workflowExecution WorkflowE } timeNow := time.Now().Unix() - //curaction := Action{ + //curaction := shuffle.Action{ // AppName: baseAction.AppName, // AppVersion: baseAction.AppVersion, // Label: baseAction.Label, // Name: baseAction.Name, // ID: baseAction.ID, //} - result := ActionResult{ + result := shuffle.ActionResult{ Action: baseAction, ExecutionId: workflowExecution.ExecutionId, Authorization: workflowExecution.Authorization, @@ -1200,7 +1210,7 @@ func removeIndex(s []string, i int) []string { return s[:len(s)-1] } -func handleExecutionResult(workflowExecution WorkflowExecution) { +func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { if len(startAction) == 0 { startAction = workflowExecution.Start if len(startAction) == 0 { @@ -1284,7 +1294,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // care if it gets stuck in a loop. // FIXME: Force killing a worker should result in a notification somewhere if len(nextActions) == 0 { - log.Printf("[INFO] No next action. Finished? Result vs Actions: %d - %d", len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) + log.Printf("[INFO] No next action. Finished? Result vs shuffle.Actions: %d - %d", len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) exit := true for _, item := range workflowExecution.Results { if item.Status == "EXECUTING" { @@ -1401,7 +1411,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { //visited = append(visited, action.ID) //executed = append(executed, action.ID) - trigger := Trigger{} + trigger := shuffle.Trigger{} for _, innertrigger := range workflowExecution.Workflow.Triggers { if innertrigger.ID == action.ID { trigger = innertrigger @@ -1410,18 +1420,18 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { } // FIXME: Add startnode from frontend - action.Parameters = []WorkflowAppActionParameter{} + action.Parameters = []shuffle.WorkflowAppActionParameter{} for _, parameter := range trigger.Parameters { parameter.Variant = "STATIC_VALUE" action.Parameters = append(action.Parameters, parameter) } - action.Parameters = append(action.Parameters, WorkflowAppActionParameter{ + action.Parameters = append(action.Parameters, shuffle.WorkflowAppActionParameter{ Name: "source_workflow", Value: workflowExecution.Workflow.ID, }) - action.Parameters = append(action.Parameters, WorkflowAppActionParameter{ + action.Parameters = append(action.Parameters, shuffle.WorkflowAppActionParameter{ Name: "source_execution", Value: workflowExecution.ExecutionId, }) @@ -1444,7 +1454,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { continue } else { log.Printf("Should stop after this iteration because it's user-input based. %#v", action) - trigger := Trigger{} + trigger := shuffle.Trigger{} for _, innertrigger := range workflowExecution.Workflow.Triggers { if innertrigger.ID == action.ID { trigger = innertrigger @@ -1573,7 +1583,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { } if len(action.Parameters) == 0 { - action.Parameters = []WorkflowAppActionParameter{} + action.Parameters = []shuffle.WorkflowAppActionParameter{} } if len(action.Errors) == 0 { @@ -1639,7 +1649,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { // If cleanup is set, it should run for efficiency pullOptions := types.ImagePullOptions{} if cleanupEnv == "true" { - err = deployApp(dockercli, images[0], identifier, env) + err = deployApp(dockercli, images[0], identifier, env, workflowExecution) if err != nil { if strings.Contains(err.Error(), "exited prematurely") { shutdown(workflowExecution, action.ID, err.Error(), true) @@ -1666,7 +1676,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { log.Printf("[INFO] Successfully downloaded %s", image) } - err = deployApp(dockercli, image, identifier, env) + err = deployApp(dockercli, image, identifier, env, workflowExecution) if err != nil { log.Printf("[ERROR] Failed deploying image for the FOURTH time. Aborting if the image doesn't exist") @@ -1683,7 +1693,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { } } else { - err = deployApp(dockercli, image, identifier, env) + err = deployApp(dockercli, image, identifier, env, workflowExecution) if err != nil { if strings.Contains(err.Error(), "exited prematurely") { shutdown(workflowExecution, action.ID, err.Error(), true) @@ -1696,7 +1706,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { image = strings.ReplaceAll(image, " ", "-") } - err = deployApp(dockercli, image, identifier, env) + err = deployApp(dockercli, image, identifier, env, workflowExecution) if err != nil { if strings.Contains(err.Error(), "exited prematurely") { shutdown(workflowExecution, action.ID, err.Error(), true) @@ -1707,7 +1717,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { image = strings.ReplaceAll(image, " ", "-") } - err = deployApp(dockercli, image, identifier, env) + err = deployApp(dockercli, image, identifier, env, workflowExecution) if err != nil { if strings.Contains(err.Error(), "exited prematurely") { shutdown(workflowExecution, action.ID, err.Error(), true) @@ -1734,7 +1744,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { log.Printf("[INFO] Successfully downloaded %s", image) } - err = deployApp(dockercli, image, identifier, env) + err = deployApp(dockercli, image, identifier, env, workflowExecution) if err != nil { log.Printf("[ERROR] Failed deploying image for the FOURTH time. Aborting if the image doesn't exist") if strings.Contains(err.Error(), "exited prematurely") { @@ -1792,7 +1802,7 @@ func handleExecutionResult(workflowExecution WorkflowExecution) { return } -func executionInit(workflowExecution WorkflowExecution) error { +func executionInit(workflowExecution shuffle.WorkflowExecution) error { parents = map[string][]string{} children = map[string][]string{} @@ -1834,10 +1844,10 @@ func executionInit(workflowExecution WorkflowExecution) error { } if trigger.ID == branch.SourceID { - log.Printf("[INFO] Trigger %s is the source!", trigger.AppName) + log.Printf("[INFO] shuffle.Trigger %s is the source!", trigger.AppName) sourceFound = true } else if trigger.ID == branch.DestinationID { - log.Printf("[INFO] Trigger %s is the destination!", trigger.AppName) + log.Printf("[INFO] shuffle.Trigger %s is the destination!", trigger.AppName) destinationFound = true } } @@ -1862,7 +1872,7 @@ func executionInit(workflowExecution WorkflowExecution) error { log.Printf("[INFO] NEXT ACTIONS: %#v\n\n", nextActions) */ - log.Printf("[INFO] Actions: %d + Special Triggers: %d", len(workflowExecution.Workflow.Actions), extra) + log.Printf("[INFO] shuffle.Actions: %d + Special shuffle.Triggers: %d", len(workflowExecution.Workflow.Actions), extra) onpremApps := []string{} toExecuteOnprem := []string{} for _, action := range workflowExecution.Workflow.Actions { @@ -1913,7 +1923,7 @@ func executionInit(workflowExecution WorkflowExecution) error { return nil } -func handleExecution(client *http.Client, req *http.Request, workflowExecution WorkflowExecution) error { +func handleExecution(client *http.Client, req *http.Request, workflowExecution shuffle.WorkflowExecution) error { // if no onprem runs (shouldn't happen, but extra check), exit // if there are some, load the images ASAP for the app @@ -1999,17 +2009,17 @@ func arrayContains(visited []string, id string) bool { return found } -func getResult(workflowExecution WorkflowExecution, id string) ActionResult { +func getResult(workflowExecution shuffle.WorkflowExecution, id string) shuffle.ActionResult { for _, actionResult := range workflowExecution.Results { if actionResult.Action.ID == id { return actionResult } } - return ActionResult{} + return shuffle.ActionResult{} } -func getAction(workflowExecution WorkflowExecution, id, environment string) Action { +func getAction(workflowExecution shuffle.WorkflowExecution, id, environment string) shuffle.Action { for _, action := range workflowExecution.Workflow.Actions { if action.ID == id { return action @@ -2018,7 +2028,7 @@ func getAction(workflowExecution WorkflowExecution, id, environment string) Acti for _, trigger := range workflowExecution.Workflow.Triggers { if trigger.ID == id { - return Action{ + return shuffle.Action{ ID: trigger.ID, AppName: trigger.AppName, Name: trigger.AppName, @@ -2029,12 +2039,12 @@ func getAction(workflowExecution WorkflowExecution, id, environment string) Acti } } - return Action{} + return shuffle.Action{} } -func runUserInput(client *http.Client, action Action, workflowId, workflowExecutionId, authorization string, configuration string) error { +func runUserInput(client *http.Client, action shuffle.Action, workflowId, workflowExecutionId, authorization string, configuration string) error { timeNow := time.Now().Unix() - result := ActionResult{ + result := shuffle.ActionResult{ Action: action, ExecutionId: workflowExecutionId, Authorization: authorization, @@ -2104,7 +2114,7 @@ func runTestExecution(client *http.Client, workflowId, apikey string) (string, s } log.Printf("[INFO] Test Body: %s", string(body)) - var workflowExecution WorkflowExecution + var workflowExecution shuffle.WorkflowExecution err = json.Unmarshal(body, &workflowExecution) if err != nil { log.Printf("Failed workflowExecution unmarshal: %s", err) @@ -2124,17 +2134,17 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { } //log.Printf("Got result: %s", string(body)) - var actionResult ActionResult + var actionResult shuffle.ActionResult err = json.Unmarshal(body, &actionResult) if err != nil { - log.Printf("Failed ActionResult unmarshaling: %s", err) + log.Printf("Failed shuffle.ActionResult unmarshaling: %s", err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return } - // 1. Get the WorkflowExecution(ExecutionId) from the database - // 2. if ActionResult.Authentication != WorkflowExecution.Authentication -> exit + // 1. Get the shuffle.WorkflowExecution(ExecutionId) from the database + // 2. if shuffle.ActionResult.Authentication != shuffle.WorkflowExecution.Authentication -> exit // 3. Add to and update actionResult in workflowExecution // 4. Push to db // IF FAIL: Set executionstatus: abort or cancel @@ -2179,7 +2189,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { //if actionResult.Status == "WAITING" && actionResult.Action.AppName == "User Input" { // log.Printf("SHOULD WAIT A BIT AND RUN CLOUD STUFF WITH USER INPUT! WAITING!") - // var trigger Trigger + // var trigger shuffle.Trigger // err = json.Unmarshal([]byte(actionResult.Result), &trigger) // if err != nil { // log.Printf("Failed unmarshaling actionresult for user input: %s", err) @@ -2199,7 +2209,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // actionResult.Result = fmt.Sprintf("Cloud error: %s", err) // workflowExecution.Results = append(workflowExecution.Results, actionResult) // workflowExecution.Status = "ABORTED" - // err = setWorkflowExecution(ctx, *workflowExecution, true) + // err = setshuffle.WorkflowExecution(ctx, *workflowExecution, true) // if err != nil { // log.Printf("Failed ") // } else { @@ -2217,7 +2227,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // workflowExecution.Results = append(workflowExecution.Results, actionResult) // workflowExecution.Status = actionResult.Status - // err = setWorkflowExecution(ctx, *workflowExecution, true) + // err = setshuffle.WorkflowExecution(ctx, *workflowExecution, true) // if err != nil { // log.Printf("Failed ") // } else { @@ -2234,7 +2244,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { } -func findChildNodes(workflowExecution WorkflowExecution, nodeId string) []string { +func findChildNodes(workflowExecution shuffle.WorkflowExecution, nodeId string) []string { //log.Printf("\nNODE TO FIX: %s\n\n", nodeId) allChildren := []string{nodeId} @@ -2282,7 +2292,7 @@ func findChildNodes(workflowExecution WorkflowExecution, nodeId string) []string } // Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times -func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workflowExecutionId string, actionResult ActionResult, resp http.ResponseWriter) { +func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workflowExecutionId string, actionResult shuffle.ActionResult, resp http.ResponseWriter) { //log.Printf("IN WORKFLOWEXECUTION SUB!") // Should start a tx for the execution here workflowExecution, err := getWorkflowExecution(ctx, workflowExecutionId) @@ -2306,7 +2316,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl //} //key := datastore.NameKey("workflowexecution", workflowExecutionId, nil) - //workflowExecution := &WorkflowExecution{} + //workflowExecution := &shuffle.WorkflowExecution{} //if err := tx.Get(key, workflowExecution); err != nil { // log.Printf("[ERROR] tx.Get bug: %v", err) // tx.Rollback() @@ -2314,7 +2324,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting the workflow key"}`))) // return //} - actionResult.Action = Action{ + actionResult.Action = shuffle.Action{ AppName: actionResult.Action.AppName, AppVersion: actionResult.Action.AppVersion, Label: actionResult.Action.Label, @@ -2326,15 +2336,15 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl if actionResult.Status == "ABORTED" || actionResult.Status == "FAILURE" { //dbSave = true - newResults := []ActionResult{} + newResults := []shuffle.ActionResult{} childNodes := []string{} if workflowExecution.Workflow.Configuration.ExitOnError { - log.Printf("[WARNING] Actionresult is %s for node %s in %s. Should set workflowExecution and exit all running functions", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) + log.Printf("[WARNING] shuffle.Actionresult is %s for node %s in %s. Should set workflowExecution and exit all running functions", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) workflowExecution.Status = actionResult.Status workflowExecution.LastNode = actionResult.Action.ID // Find underlying nodes and add them } else { - log.Printf("[WARNING] Actionresult is %s for node %s in %s. Continuing anyway because of workflow configuration.", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) + log.Printf("[WARNING] shuffle.Actionresult is %s for node %s in %s. Continuing anyway because of workflow configuration.", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) // Finds ALL childnodes to set them to SKIPPED // Remove duplicates //log.Printf("CHILD NODES: %d", len(childNodes)) @@ -2346,7 +2356,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // 1. Find the action itself // 2. Create an actionresult - curAction := Action{ID: ""} + curAction := shuffle.Action{ID: ""} for _, action := range workflowExecution.Workflow.Actions { if action.ID == nodeId { curAction = action @@ -2396,7 +2406,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } if !skipNodeAdd { - newResult := ActionResult{ + newResult := shuffle.ActionResult{ Action: curAction, ExecutionId: actionResult.ExecutionId, Authorization: actionResult.Authorization, @@ -2419,7 +2429,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Cleans up aborted, and always gives a result lastResult := "" - // type ActionResult struct { + // type shuffle.ActionResult struct { for _, result := range workflowExecution.Results { if actionResult.Action.ID == result.Action.ID { continue @@ -2501,7 +2511,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // 1. Find the action itself // 2. Create an actionresult - curAction := Action{ID: ""} + curAction := shuffle.Action{ID: ""} for _, action := range workflowExecution.Workflow.Actions { if action.ID == nodeId { curAction = action @@ -2545,7 +2555,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } if !skipNodeAdd { - newAction := Action{ + newAction := shuffle.Action{ AppName: curAction.AppName, AppVersion: curAction.AppVersion, Label: curAction.Label, @@ -2553,7 +2563,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl ID: curAction.ID, } - newResult := ActionResult{ + newResult := shuffle.ActionResult{ Action: newAction, ExecutionId: actionResult.ExecutionId, Authorization: actionResult.Authorization, @@ -2688,7 +2698,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Result string `json:"result" datastore:"result,noindex"` // Arbitrary reduction size maxSize := 500000 - newResults := []ActionResult{} + newResults := []shuffle.ActionResult{} for _, item := range workflowExecution.Results { if len(item.Result) > maxSize { item.Result = "[ERROR] Result too large to handle (https://github.com/frikky/shuffle/issues/171)" @@ -2705,7 +2715,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Handled using cachhing, so actually pretty fast cacheKey := fmt.Sprintf("workflowexecution-%s", workflowExecution.ExecutionId) if value, found := requestCache.Get(cacheKey); found { - parsedValue := value.(*WorkflowExecution) + parsedValue := value.(*shuffle.WorkflowExecution) if len(parsedValue.Results) > 0 && len(parsedValue.Results) != resultLength { setExecution = false if attempts > 5 { @@ -2731,7 +2741,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl log.Printf("[INFO] Skipping setexec with status %s", workflowExecution.Status) // Just in case. Should MAYBE validate finishing another time as well. - // This fixes issues with e.g. Action -> Trigger -> Action. + // This fixes issues with e.g. shuffle.Action -> shuffle.Trigger -> shuffle.Action. handleExecutionResult(*workflowExecution) //validateFinished(workflowExecution) } @@ -2744,21 +2754,21 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl //resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) } -func getWorkflowExecution(ctx context.Context, id string) (*WorkflowExecution, error) { +func getWorkflowExecution(ctx context.Context, id string) (*shuffle.WorkflowExecution, error) { //log.Printf("IN GET WORKFLOW EXEC!") cacheKey := fmt.Sprintf("workflowexecution-%s", id) if value, found := requestCache.Get(cacheKey); found { - parsedValue := value.(*WorkflowExecution) + parsedValue := value.(*shuffle.WorkflowExecution) //log.Printf("Found execution for id %s with %d results", parsedValue.ExecutionId, len(parsedValue.Results)) //validateFinished(*parsedValue) return parsedValue, nil } - return &WorkflowExecution{}, errors.New("No workflowexecution defined yet") + return &shuffle.WorkflowExecution{}, errors.New("No workflowexecution defined yet") } -func sendResult(workflowExecution WorkflowExecution, data []byte) { +func sendResult(workflowExecution shuffle.WorkflowExecution, data []byte) { fullUrl := fmt.Sprintf("%s/api/v1/streams", baseUrl) req, err := http.NewRequest( "POST", @@ -2786,8 +2796,8 @@ func sendResult(workflowExecution WorkflowExecution, data []byte) { } } -func validateFinished(workflowExecution WorkflowExecution) { - log.Printf("[INFO] VALIDATION. Status: %s, Actions: %d, Extra: %d, Results: %d\n", workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results)) +func validateFinished(workflowExecution shuffle.WorkflowExecution) { + log.Printf("[INFO] VALIDATION. Status: %s, shuffle.Actions: %d, Extra: %d, Results: %d\n", workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results)) //if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions)+extra { if (len(environments) == 1 && requestsSent == 0 && len(workflowExecution.Results) >= 1) || (len(workflowExecution.Results) >= len(workflowExecution.Workflow.Actions) && len(workflowExecution.Workflow.Actions) > 0) { @@ -2814,10 +2824,10 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { return } - var actionResult ActionResult + var actionResult shuffle.ActionResult err = json.Unmarshal(body, &actionResult) if err != nil { - log.Printf("Failed ActionResult unmarshaling: %s", err) + log.Printf("Failed shuffle.ActionResult unmarshaling: %s", err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return @@ -2852,7 +2862,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { } -func setWorkflowExecution(ctx context.Context, workflowExecution WorkflowExecution, dbSave bool) error { +func setWorkflowExecution(ctx context.Context, workflowExecution shuffle.WorkflowExecution, dbSave bool) error { //log.Printf("IN SET WORKFLOW EXEC!") //log.Printf("\n\n\nRESULT: %s\n\n\n", workflowExecution.Status) if len(workflowExecution.ExecutionId) == 0 { @@ -2901,7 +2911,7 @@ func getAvailablePort() (net.Listener, error) { //return fmt.Sprintf(":%d", port) } -func webserverSetup(workflowExecution WorkflowExecution) net.Listener { +func webserverSetup(workflowExecution shuffle.WorkflowExecution) net.Listener { hostname := getLocalIP() // FIXME: This MAY not work because of speed between first @@ -2972,7 +2982,7 @@ func main() { log.Printf("[INFO] Running normal execution with auth %s and ID %s", authorization, executionId) } - workflowExecution := WorkflowExecution{ + workflowExecution := shuffle.WorkflowExecution{ ExecutionId: executionId, } if len(authorization) == 0 { @@ -3052,7 +3062,8 @@ func main() { } log.Printf("Environments: %s. 1 = webserver, 0 or >1 = default", environments) - if len(environments) == 1 { //&& len(workflowExecution.Actions)+len(workflowExecution.Triggers) > 1 { + if len(environments) == 1 { //&& workflowExecution.ExecutionSource != "default" { + log.Printf("[INFO] Running OPTIMIZED execution (not manual)") listener := webserverSetup(workflowExecution) err := executionInit(workflowExecution) if err != nil { @@ -3070,6 +3081,9 @@ func main() { //wg := sync.WaitGroup{} //wg.Add(1) //wg.Wait() + } else { + log.Printf("[INFO] Running NON-OPTIMIZED execution for type %s with %d environments", workflowExecution.ExecutionSource, len(environments)) + } } From 4f49a87a21da4d023a511406c71896dda0e360cc Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 20 Mar 2021 16:17:28 +0100 Subject: [PATCH 171/185] Removed unused structs from worker --- functions/onprem/worker/worker.go | 697 ------------------------------ 1 file changed, 697 deletions(-) diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index 9966785a..c2b5e5d9 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -85,703 +85,6 @@ func init() { } } -/* -type Userapi struct { - Username string `datastore:"username"` - ApiKey string `datastore:"apikey"` -} - -type ExecutionInfo struct { - TotalApiUsage int64 `json:"total_api_usage" datastore:"total_api_usage"` - Totalshuffle.WorkflowExecutions int64 `json:"total_workflow_executions" datastore:"total_workflow_executions"` - TotalAppExecutions int64 `json:"total_app_executions" datastore:"total_app_executions"` - TotalCloudExecutions int64 `json:"total_cloud_executions" datastore:"total_cloud_executions"` - TotalOnpremExecutions int64 `json:"total_onprem_executions" datastore:"total_onprem_executions"` - DailyApiUsage int64 `json:"daily_api_usage" datastore:"daily_api_usage"` - Dailyshuffle.WorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` - DailyAppExecutions int64 `json:"daily_app_executions" datastore:"daily_app_executions"` - DailyCloudExecutions int64 `json:"daily_cloud_executions" datastore:"daily_cloud_executions"` - DailyOnpremExecutions int64 `json:"daily_onprem_executions" datastore:"daily_onprem_executions"` -} - -type StatisticsData struct { - Timestamp int64 `json:"timestamp" datastore:"timestamp"` - Id string `json:"id" datastore:"id"` - Amount int64 `json:"amount" datastore:"amount"` -} - -type StatisticsItem struct { - Total int64 `json:"total" datastore:"total"` - Fieldname string `json:"field_name" datastore:"field_name"` - Data []StatisticsData `json:"data" datastore:"data"` -} -*/ - -// "Execution by status" -// Execution history -//type GlobalStatistics struct { -// BackendExecutions int64 `json:"backend_executions" datastore:"backend_executions"` -// WorkflowCount int64 `json:"workflow_count" datastore:"workflow_count"` -// ExecutionCount int64 `json:"execution_count" datastore:"execution_count"` -// ExecutionSuccessCount int64 `json:"execution_success_count" datastore:"execution_success_count"` -// ExecutionAbortCount int64 `json:"execution_abort_count" datastore:"execution_abort_count"` -// ExecutionFailureCount int64 `json:"execution_failure_count" datastore:"execution_failure_count"` -// ExecutionPendingCount int64 `json:"execution_pending_count" datastore:"execution_pending_count"` -// AppUsageCount int64 `json:"app_usage_count" datastore:"app_usage_count"` -// TotalAppsCount int64 `json:"total_apps_count" datastore:"total_apps_count"` -// SelfMadeAppCount int64 `json:"self_made_app_count" datastore:"self_made_app_count"` -// WebhookUsageCount int64 `json:"webhook_usage_count" datastore:"webhook_usage_count"` -// Baseline map[string]int64 `json:"baseline" datastore:"baseline"` -//} - -/* -type ParsedOpenApi struct { - Body string `datastore:"body,noindex" json:"body"` - ID string `datastore:"id" json:"id"` - Success bool `datastore:"success,omitempty" json:"success,omitempty"` -} - -// Limits set for a user so that they can't do a shitload -type UserLimits struct { - DailyApiUsage int64 `json:"daily_api_usage" datastore:"daily_api_usage"` - Dailyshuffle.WorkflowExecutions int64 `json:"daily_workflow_executions" datastore:"daily_workflow_executions"` - DailyCloudExecutions int64 `json:"daily_cloud_executions" datastore:"daily_cloud_executions"` - DailyTriggers int64 `json:"daily_triggers" datastore:"daily_triggers"` - DailyMailUsage int64 `json:"daily_mail_usage" datastore:"daily_mail_usage"` - MaxTriggers int64 `json:"max_triggers" datastore:"max_triggers"` - MaxWorkflows int64 `json:"max_workflows" datastore:"max_workflows"` -} - -type retStruct struct { - Success bool `json:"success"` - SyncFeatures SyncFeatures `json:"sync_features"` - SessionKey string `json:"session_key"` - IntervalSeconds int64 `json:"interval_seconds"` - Reason string `json:"reason"` -} - -// Saves some data, not sure what to have here lol -type UserAuth struct { - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Name string `json:"name" datastore:"name" yaml:"name"` - Workflows []string `json:"workflows" datastore:"workflows"` - Username string `json:"username" datastore:"username"` - Fields []UserAuthField `json:"fields" datastore:"fields"` -} - -type UserAuthField struct { - Key string `json:"key" datastore:"key"` - Value string `json:"value" datastore:"value,noindex"` -} - -// Not environment, but execution environment -type Environment struct { - Name string `datastore:"name"` - Type string `datastore:"type"` - Registered bool `datastore:"registered"` - Default bool `datastore:"default" json:"default"` - Archived bool `datastore:"archived" json:"archived"` - Id string `datastore:"id" json:"id"` - OrgId string `datastore:"org_id" json:"org_id"` -} - -type User struct { - Username string `datastore:"Username" json:"username"` - Password string `datastore:"password,noindex" password:"password,omitempty"` - Session string `datastore:"session,noindex" json:"session"` - Verified bool `datastore:"verified,noindex" json:"verified"` - PrivateApps []WorkflowApp `datastore:"privateapps" json:"privateapps":` - Role string `datastore:"role" json:"role"` - Roles []string `datastore:"roles" json:"roles"` - VerificationToken string `datastore:"verification_token" json:"verification_token"` - ApiKey string `datastore:"apikey" json:"apikey"` - ResetReference string `datastore:"reset_reference" json:"reset_reference"` - Executions ExecutionInfo `datastore:"executions" json:"executions"` - Limits UserLimits `datastore:"limits" json:"limits"` - Authentication []UserAuth `datastore:"authentication,noindex" json:"authentication"` - ResetTimeout int64 `datastore:"reset_timeout,noindex" json:"reset_timeout"` - Id string `datastore:"id" json:"id"` - Orgs []string `datastore:"orgs" json:"orgs"` - CreationTime int64 `datastore:"creation_time" json:"creation_time"` - ActiveOrg Org `json:"active_org" datastore:"active_org"` - Active bool `datastore:"active" json:"active"` -} - -// timeout maybe? idk -type session struct { - Username string `datastore:"Username,noindex"` - Id string `datastore:"Id,noindex"` - Session string `datastore:"session,noindex"` -} - -type loginStruct struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type Contact struct { - Firstname string `json:"firstname"` - Lastname string `json:"lastname"` - Title string `json:"title"` - Companyname string `json:"companyname"` - Phone string `json:"phone"` - Email string `json:"email"` - Message string `json:"message"` -} - -type Translator struct { - Src struct { - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - Description string `json:"description" datastore:"description,noindex"` - Required string `json:"required" datastore:"required"` - Type string `json:"type" datastore:"type"` - Schema struct { - Type string `json:"type" datastore:"type"` - } `json:"schema" datastore:"schema"` - } `json:"src" datastore:"src"` - Dst struct { - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - Type string `json:"type" datastore:"type"` - Description string `json:"description" datastore:"description,noindex"` - Required string `json:"required" datastore:"required"` - Schema struct { - Type string `json:"type" datastore:"type"` - } `json:"schema" datastore:"schema"` - } `json:"dst" datastore:"dst"` -} - -type Appconfig struct { - Key string `json:"key" datastore:"key"` - Value string `json:"value" datastore:"value,noindex"` -} - -type ScheduleApp struct { - Foldername string `json:"foldername" datastore:"foldername,noindex"` - Name string `json:"name" datastore:"name,noindex"` - Id string `json:"id" datastore:"id,noindex"` - Description string `json:"description" datastore:"description,noindex"` - Action string `json:"action" datastore:"action,noindex"` - Config []Appconfig `json:"config,omitempty" datastore:"config,noindex"` -} - -type AppInfo struct { - SourceApp ScheduleApp `json:"sourceapp,omitempty" datastore:"sourceapp,noindex"` - DestinationApp ScheduleApp `json:"destinationapp,omitempty" datastore:"destinationapp,noindex"` -} - -// May 2020: Reused for onprem schedules - Id, Seconds, WorkflowId and argument -type ScheduleOld struct { - Id string `json:"id" datastore:"id"` - StartNode string `json:"start_node" datastore:"start_node"` - Seconds int `json:"seconds" datastore:"seconds"` - WorkflowId string `json:"workflow_id" datastore:"workflow_id", ` - Argument string `json:"argument" datastore:"argument"` - WrappedArgument string `json:"wrapped_argument" datastore:"wrapped_argument"` - AppInfo AppInfo `json:"appinfo" datastore:"appinfo,noindex"` - Finished bool `json:"finished" finished:"id"` - BaseAppLocation string `json:"base_app_location" datastore:"baseapplocation,noindex"` - Translator []Translator `json:"translator,omitempty" datastore:"translator"` - Org string `json:"org" datastore:"org"` - CreatedBy string `json:"createdby" datastore:"createdby"` - Availability string `json:"availability" datastore:"availability"` - CreationTime int64 `json:"creationtime" datastore:"creationtime,noindex"` - LastModificationtime int64 `json:"lastmodificationtime" datastore:"lastmodificationtime,noindex"` - LastRuntime int64 `json:"lastruntime" datastore:"lastruntime,noindex"` - Frequency string `json:"frequency" datastore:"frequency,noindex"` - Environment string `json:"environment" datastore:"environment"` -} - -// Returned from /GET /schedules -type Schedules struct { - Schedules []ScheduleOld `json:"schedules"` - Success bool `json:"success"` -} - -type ScheduleApps struct { - Apps []ApiYaml `json:"apps"` - Success bool `json:"success"` -} - -// The yaml that is uploaded -type ApiYaml struct { - Name string `json:"name" yaml:"name" required:"true datastore:"name"` - Foldername string `json:"foldername" yaml:"foldername" required:"true datastore:"foldername"` - Id string `json:"id" yaml:"id",required:"true, datastore:"id"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - AppVersion string `json:"app_version" yaml:"app_version",datastore:"app_version"` - ContactInfo struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Url string `json:"url" datastore:"url" yaml:"url"` - } `json:"contact_info" datastore:"contact_info" yaml:"contact_info"` - Types []string `json:"types" datastore:"types" yaml:"types"` - Input []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - InputParameters []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Required string `json:"required" datastore:"required" yaml:"required"` - Schema struct { - Type string `json:"type" datastore:"type" yaml:"type"` - } `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"inputparameters" datastore:"inputparameters" yaml:"inputparameters"` - OutputParameters []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Required string `json:"required" datastore:"required" yaml:"required"` - Schema struct { - Type string `json:"type" datastore:"type" yaml:"type"` - } `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"outputparameters" datastore:"outputparameters" yaml:"outputparameters"` - Config []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Required string `json:"required" datastore:"required" yaml:"required"` - Schema struct { - Type string `json:"type" datastore:"type" yaml:"type"` - } `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"config" datastore:"config" yaml:"config"` - } `json:"input" datastore:"input" yaml:"input"` - Output []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Config []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Required string `json:"required" datastore:"required" yaml:"required"` - Schema struct { - Type string `json:"type" datastore:"type" yaml:"type"` - } `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"config" datastore:"config" yaml:"config"` - InputParameters []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Required string `json:"required" datastore:"required" yaml:"required"` - Schema struct { - Type string `json:"type" datastore:"type" yaml:"type"` - } `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"inputparameters" datastore:"inputparameters" yaml:"inputparameters"` - OutputParameters []struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - Required string `json:"required" datastore:"required" yaml:"required"` - Schema struct { - Type string `json:"type" datastore:"type" yaml:"type"` - } `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"outputparameters" datastore:"outputparameters" yaml:"outputparameters"` - } `json:"output" datastore:"output" yaml:"output"` -} - -type Hooks struct { - Hooks []Hook `json:"hooks"` - Success bool `json:"-"` -} - -type Info struct { - Url string `json:"url" datastore:"url"` - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description,noindex"` -} - -// shuffle.Actions to be done by webhooks etc -// Field is the actual field to use from json -type HookAction struct { - Type string `json:"type" datastore:"type"` - Name string `json:"name" datastore:"name"` - Id string `json:"id" datastore:"id"` - Field string `json:"field" datastore:"field"` -} - -type Hook struct { - Id string `json:"id" datastore:"id"` - Start string `json:"start" datastore:"start"` - Info Info `json:"info" datastore:"info"` - Actions []HookAction `json:"actions" datastore:"actions,noindex"` - Type string `json:"type" datastore:"type"` - Owner string `json:"owner" datastore:"owner"` - Status string `json:"status" datastore:"status"` - Workflows []string `json:"workflows" datastore:"workflows"` - Running bool `json:"running" datastore:"running"` - OrgId string `json:"org_id" datastore:"org_id"` - Environment string `json:"environment" datastore:"environment"` -} - -type ExecutionRequest struct { - ExecutionId string `json:"execution_id,omitempty"` - ExecutionArgument string `json:"execution_argument,omitempty"` - ExecutionSource string `json:"execution_source,omitempty"` - WorkflowId string `json:"workflow_id,omitempty"` - Environments []string `json:"environments,omitempty"` - Authorization string `json:"authorization,omitempty"` - Status string `json:"status,omitempty"` - Start string `json:"start,omitempty"` - Type string `json:"type,omitempty"` -} - -type SyncFeatures struct { - Webhook SyncData `json:"webhook" datastore:"webhook"` - Schedules SyncData `json:"schedules" datastore:"schedules"` - UserInput SyncData `json:"user_input" datastore:"user_input"` - SendMail SyncData `json:"send_mail" datastore:"send_mail"` - SendSms SyncData `json:"send_sms" datastore:"send_sms"` - Updates SyncData `json:"updates" datastore:"updates"` - Notifications SyncData `json:"notifications" datastore:"notifications"` - EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` - AppExecutions SyncData `json:"app_executions" datastore:"app_executions"` - shuffle.WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` - Apps SyncData `json:"apps" datastore:"apps"` - Workflows SyncData `json:"workflows" datastore:"workflows"` - Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` - Authentication SyncData `json:"authentication" datastore:"authentication"` - Schedule SyncData `json:"schedule" datastore:"schedule"` -} - -type SyncData struct { - Active bool `json:"active" datastore:"active"` - Type string `json:"type,omitempty" datastore:"type"` - Name string `json:"name,omitempty" datastore:"name"` - Description string `json:"description,omitempty" datastore:"description"` - Limit int64 `json:"limit,omitempty" datastore:"limit"` - StartDate int64 `json:"start_date,omitempty" datastore:"start_date"` - EndDate int64 `json:"end_date,omitempty" datastore:"end_date"` - DataCollection int64 `json:"data_collection,omitempty" datastore:"data_collection"` -} - -type SyncConfig struct { - Interval int64 `json:"interval" datastore:"interval"` - Apikey string `json:"api_key" datastore:"api_key"` -} - -// Role is just used for feedback for a user -type Org struct { - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description"` - Image string `json:"image" datastore:"image,noindex"` - Id string `json:"id" datastore:"id"` - Org string `json:"org" datastore:"org"` - Users []User `json:"users" datastore:"users"` - Role string `json:"role" datastore:"role"` - Roles []string `json:"roles" datastore:"roles"` - CloudSync bool `json:"cloud_sync" datastore:"CloudSync"` - SyncConfig SyncConfig `json:"sync_config" datastore:"sync_config"` - SyncFeatures SyncFeatures `json:"sync_features" datastore:"sync_features"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` -} - -type AppAuthenticationStorage struct { - Active bool `json:"active" datastore:"active"` - Label string `json:"label" datastore:"label"` - Id string `json:"id" datastore:"id"` - App WorkflowApp `json:"app" datastore:"app,noindex"` - Fields []AuthenticationStore `json:"fields" datastore:"fields"` - Usage []AuthenticationUsage `json:"usage" datastore:"usage"` - WorkflowCount int64 `json:"workflow_count" datastore:"workflow_count"` - NodeCount int64 `json:"node_count" datastore:"node_count"` - OrgId string `json:"org_id" datastore:"org_id"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` -} - -type AuthenticationUsage struct { - WorkflowId string `json:"workflow_id" datastore:"workflow_id"` - Nodes []string `json:"nodes" datastore:"nodes"` -} - -// An app inside Shuffle -// Source string `json:"source" datastore:"soure" yaml:"source"` - downloadlocation -type WorkflowApp struct { - Name string `json:"name" yaml:"name" required:true datastore:"name"` - IsValid bool `json:"is_valid" yaml:"is_valid" required:true datastore:"is_valid"` - ID string `json:"id" yaml:"id,omitempty" required:false datastore:"id"` - Link string `json:"link" yaml:"link" required:false datastore:"link,noindex"` - AppVersion string `json:"app_version" yaml:"app_version" required:true datastore:"app_version"` - SharingConfig string `json:"sharing_config" yaml:"sharing_config" datastore:"sharing_config"` - Generated bool `json:"generated" yaml:"generated" required:false datastore:"generated"` - Downloaded bool `json:"downloaded" yaml:"downloaded" required:false datastore:"downloaded"` - Sharing bool `json:"sharing" yaml:"sharing" required:false datastore:"sharing"` - Verified bool `json:"verified" yaml:"verified" required:false datastore:"verified"` - Activated bool `json:"activated" yaml:"activated" required:false datastore:"activated"` - Tested bool `json:"tested" yaml:"tested" required:false datastore:"tested"` - Owner string `json:"owner" datastore:"owner" yaml:"owner"` - Hash string `json:"hash" datastore:"hash" yaml:"hash"` // api.yaml+dockerfile+src/app.py for apps - PrivateID string `json:"private_id" yaml:"private_id" required:false datastore:"private_id"` - Description string `json:"description" datastore:"description,noindex" required:false yaml:"description"` - Environment string `json:"environment" datastore:"environment" required:true yaml:"environment"` - SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` - LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` - ContactInfo struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Url string `json:"url" datastore:"url" yaml:"url"` - } `json:"contact_info" datastore:"contact_info" yaml:"contact_info" required:false` - Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions,noindex"` - Authentication Authentication `json:"authentication" yaml:"authentication" required:false datastore:"authentication"` - Tags []string `json:"tags" yaml:"tags" required:false datastore:"activated"` - Categories []string `json:"categories" yaml:"categories" required:false datastore:"categories"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` - LastRuntime int64 `json:"last_runtime" datastore:"last_runtime"` -} - -type shuffle.WorkflowAppActionParameter struct { - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - ID string `json:"id" datastore:"id" yaml:"id,omitempty"` - Name string `json:"name" datastore:"name" yaml:"name"` - Example string `json:"example" datastore:"example,noindex" yaml:"example"` - Value string `json:"value" datastore:"value,noindex" yaml:"value,omitempty"` - Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` - Options []string `json:"options" datastore:"options" yaml:"options"` - ActionField string `json:"action_field" datastore:"action_field" yaml:"actionfield,omitempty"` - Variant string `json:"variant" datastore:"variant" yaml:"variant,omitempty"` - Required bool `json:"required" datastore:"required" yaml:"required"` - Configuration bool `json:"configuration" datastore:"configuration" yaml:"configuration"` - Tags []string `json:"tags" datastore:"tags" yaml:"tags"` - Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` - SkipMulticheck bool `json:"skip_multicheck" datastore:"skip_multicheck" yaml:"skip_multicheck"` - ValueReplace []Valuereplace `json:"value_replace" datastore:"value_replace,noindex" yaml:"value_replace,omitempty"` - UniqueToggled bool `json:"unique_toggled" datastore:"unique_toggled" yaml:"unique_toggled"` -} - -type Valuereplace struct { - Key string `json:"key" datastore:"key" yaml:"key"` - Value string `json:"value" datastore:"value" yaml:"value"` -} - -type SchemaDefinition struct { - Type string `json:"type" datastore:"type"` -} - -type WorkflowAppAction struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id" yaml:"id,omitempty"` - Name string `json:"name" datastore:"name"` - Label string `json:"label" datastore:"label"` - NodeType string `json:"node_type" datastore:"node_type"` - Environment string `json:"environment" datastore:"environment"` - Sharing bool `json:"sharing" datastore:"sharing"` - PrivateID string `json:"private_id" datastore:"private_id"` - AppID string `json:"app_id" datastore:"app_id"` - Tags []string `json:"tags" datastore:"tags" yaml:"tags"` - Authentication []AuthenticationStore `json:"authentication" datastore:"authentication,noindex" yaml:"authentication,omitempty"` - Tested bool `json:"tested" datastore:"tested" yaml:"tested"` - Parameters []shuffle.WorkflowAppActionParameter `json:"parameters" datastore: "parameters"` - ExecutionVariable struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"execution_variable" datastore:"execution_variables"` - Returns struct { - Description string `json:"description" datastore:"returns" yaml:"description,omitempty"` - Example string `json:"example" datastore:"example" yaml:"example"` - ID string `json:"id" datastore:"id" yaml:"id,omitempty"` - Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"returns" datastore:"returns"` - AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` - Example string `json:"example" datastore:"example" yaml:"example"` - AuthNotRequired bool `json:"auth_not_required" datastore:"auth_not_required" yaml:"auth_not_required"` -} - -type shuffle.WorkflowExecution struct { - Type string `json:"type" datastore:"type"` - Status string `json:"status" datastore:"status"` - Start string `json:"start" datastore:"start"` - ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` - ExecutionId string `json:"execution_id" datastore:"execution_id"` - ExecutionSource string `json:"execution_source" datastore:"execution_source"` - ExecutionParent string `json:"execution_parent" datastore:"execution_parent"` - ExecutionOrg string `json:"execution_org" datastore:"execution_org"` - WorkflowId string `json:"workflow_id" datastore:"workflow_id"` - LastNode string `json:"last_node" datastore:"last_node"` - Authorization string `json:"authorization" datastore:"authorization"` - Result string `json:"result" datastore:"result,noindex"` - StartedAt int64 `json:"started_at" datastore:"started_at"` - CompletedAt int64 `json:"completed_at" datastore:"completed_at"` - ProjectId string `json:"project_id" datastore:"project_id"` - Locations []string `json:"locations" datastore:"locations"` - Workflow Workflow `json:"workflow" datastore:"workflow,noindex"` - Results []ActionResult `json:"results" datastore:"results,noindex"` - ExecutionVariables []struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"execution_variables,omitempty" datastore:"execution_variables,omitempty"` - OrgId string `json:"org_id" datastore:"org_id"` -} - -type shuffle.Action struct { - AppName string `json:"app_name,omitempty" datastore:"app_name"` - AppVersion string `json:"app_version,omitempty" datastore:"app_version"` - AppID string `json:"app_id,omitempty" datastore:"app_id"` - Errors []string `json:"errors,omitempty" datastore:"errors"` - ID string `json:"id,omitempty" datastore:"id"` - IsValid bool `json:"is_valid,omitempty" datastore:"is_valid"` - IsStartNode bool `json:"isStartNode,omitempty" datastore:"isStartNode"` - Sharing bool `json:"sharing,omitempty" datastore:"sharing"` - PrivateID string `json:"private_id,omitempty" datastore:"private_id"` - Label string `json:"label,omitempty" datastore:"label"` - SmallImage string `json:"small_image,omitempty" datastore:"small_image,noindex" required:false yaml:"small_image"` - LargeImage string `json:"large_image,omitempty" datastore:"large_image,noindex" yaml:"large_image" required:false` - Environment string `json:"environment,omitempty" datastore:"environment"` - Name string `json:"name,omitempty" datastore:"name"` - Parameters []shuffle.WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` - ExecutionVariable struct { - Description string `json:"description,omitempty" datastore:"description,noindex"` - ID string `json:"id,omitempty" datastore:"id"` - Name string `json:"name,omitempty" datastore:"name"` - Value string `json:"value,omitempty" datastore:"value,noindex"` - } `json:"execution_variable,omitempty" datastore:"execution_variable,omitempty"` - Position struct { - X float64 `json:"x,omitempty" datastore:"x"` - Y float64 `json:"y,omitempty" datastore:"y"` - } `json:"position,omitempty"` - Priority int `json:"priority,omitempty" datastore:"priority"` - AuthenticationId string `json:"authentication_id,omitempty" datastore:"authentication_id"` - Example string `json:"example,omitempty" datastore:"example"` - AuthNotRequired bool `json:"auth_not_required,omitempty" datastore:"auth_not_required" yaml:"auth_not_required"` -} - -// Added environment for location to execute -type shuffle.Trigger struct { - AppName string `json:"app_name" datastore:"app_name"` - Description string `json:"description" datastore:"description,noindex"` - LongDescription string `json:"long_description" datastore:"long_description"` - Status string `json:"status" datastore:"status"` - AppVersion string `json:"app_version" datastore:"app_version"` - Errors []string `json:"errors" datastore:"errors"` - ID string `json:"id" datastore:"id"` - IsValid bool `json:"is_valid" datastore:"is_valid"` - IsStartNode bool `json:"isStartNode" datastore:"isStartNode"` - Label string `json:"label" datastore:"label"` - SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` - LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` - Environment string `json:"environment" datastore:"environment"` - TriggerType string `json:"trigger_type" datastore:"trigger_type"` - Name string `json:"name" datastore:"name"` - Tags []string `json:"tags" datastore:"tags" yaml:"tags"` - Parameters []shuffle.WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` - Position struct { - X float64 `json:"x" datastore:"x"` - Y float64 `json:"y" datastore:"y"` - } `json:"position"` - Priority int `json:"priority" datastore:"priority"` -} - -type Branch struct { - DestinationID string `json:"destination_id" datastore:"destination_id"` - ID string `json:"id" datastore:"id"` - SourceID string `json:"source_id" datastore:"source_id"` - Label string `json:"label" datastore:"label"` - HasError bool `json:"has_errors" datastore: "has_errors"` - Conditions []Condition `json:"conditions" datastore: "conditions,noindex"` -} - -// Same format for a lot of stuff -type Condition struct { - Condition shuffle.WorkflowAppActionParameter `json:"condition" datastore:"condition"` - Source shuffle.WorkflowAppActionParameter `json:"source" datastore:"source"` - Destination shuffle.WorkflowAppActionParameter `json:"destination" datastore:"destination"` -} - -type Schedule struct { - Name string `json:"name" datastore:"name"` - Frequency string `json:"frequency" datastore:"frequency"` - ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` - Id string `json:"id" datastore:"id"` - OrgId string `json:"org_id" datastore:"org_id"` - Environment string `json:"environment" datastore:"environment"` -} - -type Workflow struct { - Actions []Action `json:"actions" datastore:"actions,noindex"` - Branches []Branch `json:"branches" datastore:"branches,noindex"` - Triggers []Trigger `json:"triggers" datastore:"triggers,noindex"` - Schedules []Schedule `json:"schedules" datastore:"schedules,noindex"` - Configuration struct { - ExitOnError bool `json:"exit_on_error" datastore:"exit_on_error"` - StartFromTop bool `json:"start_from_top" datastore:"start_from_top"` - } `json:"configuration,omitempty" datastore:"configuration"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` - LastRuntime int64 `json:"last_runtime" datastore:"last_runtime"` - Errors []string `json:"errors,omitempty" datastore:"errors"` - Tags []string `json:"tags,omitempty" datastore:"tags"` - ID string `json:"id" datastore:"id"` - IsValid bool `json:"is_valid" datastore:"is_valid"` - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description,noindex"` - Start string `json:"start" datastore:"start"` - Owner string `json:"owner" datastore:"owner"` - Sharing string `json:"sharing" datastore:"sharing"` - Org []Org `json:"org,omitempty" datastore:"org"` - ExecutingOrg Org `json:"execution_org,omitempty" datastore:"execution_org"` - OrgId string `json:"org_id,omitempty" datastore:"org_id"` - WorkflowVariables []struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"workflow_variables" datastore:"workflow_variables"` - ExecutionVariables []struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"execution_variables,omitempty" datastore:"execution_variables"` - ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` -} - -type shuffle.ActionResult struct { - Action shuffle.Action `json:"action" datastore:"action,noindex"` - ExecutionId string `json:"execution_id" datastore:"execution_id"` - Authorization string `json:"authorization" datastore:"authorization"` - Result string `json:"result" datastore:"result,noindex"` - StartedAt int64 `json:"started_at" datastore:"started_at"` - CompletedAt int64 `json:"completed_at" datastore:"completed_at"` - Status string `json:"status" datastore:"status"` -} - -type Authentication struct { - Required bool `json:"required" datastore:"required" yaml:"required" ` - Parameters []AuthenticationParams `json:"parameters" datastore:"parameters" yaml:"parameters"` -} - -type AuthenticationParams struct { - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - ID string `json:"id" datastore:"id" yaml:"id"` - Name string `json:"name" datastore:"name" yaml:"name"` - Example string `json:"example" datastore:"example" yaml:"example"` - Value string `json:"value,omitempty" datastore:"value,noindex" yaml:"value"` - Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` - Required bool `json:"required" datastore:"required" yaml:"required"` - In string `json:"in" datastore:"in" yaml:"in"` - Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` - Scheme string `json:"scheme" datastore:"scheme" yaml:"scheme"` // Deprecated -} - -type AuthenticationStore struct { - Key string `json:"key" datastore:"key"` - Value string `json:"value" datastore:"value,noindex"` -} - -type ExecutionRequestWrapper struct { - Data []ExecutionRequest `json:"data"` -} - -type AppExecutionExample struct { - AppName string `json:"app_name" datastore:"app_name"` - AppVersion string `json:"app_version" datastore:"app_version"` - AppAction string `json:"app_action" datastore:"app_action"` - AppId string `json:"app_id" datastore:"app_id"` - ExampleId string `json:"example_id" datastore:"example_id"` - SuccessExamples []string `json:"success_examples" datastore:"success_examples,noindex"` - FailureExamples []string `json:"failure_examples" datastore:"failure_examples,noindex"` -} -*/ - // removes every container except itself (worker) func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason string, handleResultSend bool) { log.Printf("[INFO] Shutdown (%s) started with reason %s", workflowExecution.Status, reason) From 6574f122f18869b6f1617452f57cb86f1285e04e Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 21 Mar 2021 18:01:09 +0100 Subject: [PATCH 172/185] Upgrade pre shuffle-shared merge --- backend/app_sdk/app_base.py | 6 +-- backend/go-app/codegen.go | 14 +++---- backend/go-app/main.go | 28 ++++++++++++- backend/go-app/walkoff.go | 10 ++++- frontend/src/defaultCytoscapeStyle.js | 14 +++++-- frontend/src/views/AngularWorkflow.jsx | 55 +++++++++++++++++++------- 6 files changed, 94 insertions(+), 33 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index c18be280..5e2cdd3e 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -141,11 +141,11 @@ class AppBase: print(f"VALUE APPEND: {value}") param_value += value - if param["name"] not in param_names: param_names.append(param["name"]) + except (KeyError, NameError) as e: - print(f"Key/NameError in param handler: {e}") + print(f"""Key/NameError in param handler for {param["name"]}: {e}""") print(f"OUTER VALUE: {param_value}") if len(param_value) > 0: @@ -2292,7 +2292,7 @@ class AppBase: if action_result["result"] == "": action_result["result"] = result - self.logger.debug(f"Executed {action['label']}-{action['id']} with result: {result}") + self.logger.debug(f"Executed {action['label']}-{action['id']}")#with result: {result}") #self.logger.debug(f"Data: %s" % action_result) except TypeError as e: print("TypeError issue: %s" % e) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index b0b9b14d..5943d4bd 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -429,15 +429,13 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet ) // Use lowercase when checking - /* - if strings.Contains(functionname, "login") { - //log.Printf("FUNCTION: %s", data) - log.Println(data) - log.Printf("Queries: %s", queryString) - } - */ - //log.Printf(data) + if strings.Contains(functionname, "attachment") { + //log.Printf("FUNCTION: %s", data) + //log.Println(data) + //log.Printf("Queries: %s", queryString) + } + return functionname, data } diff --git a/backend/go-app/main.go b/backend/go-app/main.go index abb13c8b..deeb5bc8 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3405,7 +3405,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { //resp.WriteHeader(200) //resp.Write([]byte(`{"success": true}`)) if hook.Status == "stopped" { - log.Printf("Not running %s because hook status is stopped", hook.Id) + log.Printf("[WARNING] Not running %s because hook status is stopped", hook.Id) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Click start to start it"}`))) return @@ -3436,10 +3436,34 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { return } + //log.Printf("BODY: %s", parsedBody) + + // This is a specific fix for MSteams and may fix other things as well + // Scared whether it may stop other things though, but that's a future problem + // (famous last words) + parsedBody := string(body) + if strings.Contains(parsedBody, "choice") { + if strings.Count(parsedBody, `\\n`) > 2 { + parsedBody = strings.Replace(parsedBody, `\\n`, "", -1) + } + if strings.Count(parsedBody, `\u0022`) > 2 { + parsedBody = strings.Replace(parsedBody, `\u0022`, `"`, -1) + } + if strings.Count(parsedBody, `\\"`) > 2 { + parsedBody = strings.Replace(parsedBody, `\\"`, `"`, -1) + } + + if strings.Contains(parsedBody, `"extra": "{`) { + parsedBody = strings.Replace(parsedBody, `"extra": "{`, `"extra": {`, 1) + parsedBody = strings.Replace(parsedBody, `}"}`, `}}`, 1) + } + } + + //log.Printf("\n\nPARSEDBODY: %s", parsedBody) newBody := ExecutionStruct{ Start: hook.Start, ExecutionSource: "webhook", - ExecutionArgument: string(body), + ExecutionArgument: parsedBody, } b, err := json.Marshal(newBody) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 456ecb4f..8af8055e 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -981,6 +981,10 @@ func validateNewWorkerExecution(body []byte) error { return errors.New(fmt.Sprintf("Bad length of trigger: %d (probably normal app)", len(execution.Workflow.Triggers))) } + if baseExecution.Status != "WAITING" && baseExecution.Status != "EXECUTING" { + return errors.New(fmt.Sprintf("Workflow is already finished or failed. Can't update")) + } + if execution.Status == "EXECUTING" { //log.Printf("[INFO] Inside executing.") extra := 0 @@ -994,6 +998,8 @@ func validateNewWorkerExecution(body []byte) error { if len(execution.Workflow.Actions)+extra == len(execution.Results) { execution.Status = "FINISHED" } + + log.Printf("BASEEXECUTION LENGTH: %d", len(baseExecution.Workflow.Actions)+extra) } // FIXME: Add extra here @@ -3699,14 +3705,14 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf Status: "SKIPPED", }) } else { - log.Printf("SHOULD KEEP TRIGGER %s", trigger.ID) + //log.Printf("SHOULD KEEP TRIGGER %s", trigger.ID) } } } //childNodes := findChildNodes(workflowExecution, workflowExecution.Start) if !startFound { - log.Printf("Startnode %s doesn't exist!", workflowExecution.Start) + log.Printf("[ERROR] Startnode %s doesn't exist!!", workflowExecution.Start) return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf(`Workflow start node "%s" doesn't exist. Exiting!`, workflowExecution.Start)) } diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index 90c7aae8..ff9990a9 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -164,7 +164,7 @@ const data = [{ css: { 'background-color': '#ffef47', 'border-color': '#ffef47', - 'border-width': '5px', + 'border-width': '8px', 'transition-property': 'border-width', 'transition-duration': '0.25s', }, @@ -212,10 +212,13 @@ const data = [{ selector: 'edge.success-highlight', css: { 'width': '5px', - 'target-arrow-color': '#399645', - 'line-color': '#399645', + 'target-arrow-color': '#41dcab', + 'line-color': '#41dcab', 'transition-property': 'line-color, width', 'transition-duration': '0.5s', + "line-fill": "linear-gradient", + "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": ["#41dcab", "#41dcab"], }, }, { @@ -223,7 +226,10 @@ const data = [{ css: { 'target-arrow-color': '#991818', 'line-color': '#991818', - 'line-style': 'dashed' + 'line-style': 'dashed', + "line-fill": "linear-gradient", + "line-gradient-stop-positions": ["0.0", "100"], + "line-gradient-stop-colors": ["#991818", "#991818"], }, }, { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 40f9b948..4cd4939a 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -447,7 +447,7 @@ const AngularWorkflow = (props) => { return response.json() }) .then((responseJson) => { - handleUpdateResults(responseJson) + handleUpdateResults(responseJson, executionRequest) }) .catch(error => { console.log("Error: ", error) @@ -484,7 +484,7 @@ const AngularWorkflow = (props) => { // Controls the colors and direction of execution results. // Style is in defaultCytoscapeStyle.js - const handleUpdateResults = (responseJson) => { + const handleUpdateResults = (responseJson, executionRequest) => { //console.log(responseJson) // Loop nodes and find results // Update on every interval? idk @@ -496,11 +496,16 @@ const AngularWorkflow = (props) => { setExecutionData(responseJson) } } + + //console.log("PRE LOOPING RESULTS: !", responseJson.execution_id, executionRequest.execution_id) + if (responseJson.execution_id !== executionRequest.execution_id) { cy.elements().removeClass('success-highlight failure-highlight executing-highlight') return } + //console.log("LOOPING RESULTS!") + if (responseJson.results !== null && responseJson.results !== []) { for (var key in responseJson.results) { var item = responseJson.results[key] @@ -562,6 +567,8 @@ const AngularWorkflow = (props) => { currentnode.removeClass('shuffle-hover-highlight') currentnode.removeClass('awaiting-data-highlight') currentnode.addClass('success-highlight') + incomingEdges.addClass('success-highlight') + outgoingEdges.addClass('success-highlight') if (visited !== undefined && visited !== null && !visited.includes(item.action.label)) { if (executionRunning) { @@ -603,7 +610,7 @@ const AngularWorkflow = (props) => { currentnode.addClass('failure-highlight') if (!visited.includes(item.action.label)) { - if (!item.action.result.includes("failed condition")) { + if (item.action.result !== undefined && item.action.result !== null && !item.action.result.includes("failed condition")) { alert.error("Error for "+item.action.label+" with result "+item.result) } visited.push(item.action.label) @@ -4014,7 +4021,7 @@ const AngularWorkflow = (props) => { } } }}> - + @@ -4029,10 +4036,16 @@ const AngularWorkflow = (props) => {
    -
    - +
    + {selectedAction.id === workflow.start ? null : + + + + }
    @@ -6690,17 +6703,21 @@ const AngularWorkflow = (props) => { {}} onMouseOut={() => {}} onClick={() => { - if (data.result === undefined || data.result === null || data.result.length === 0) { - setExecutionRequest({ - "execution_id": data.execution_id, - "authorization": data.authorization, - }) + if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") { + start() setExecutionRunning(true) setExecutionRequestStarted(false) } + + const cur_execution = { + "execution_id": data.execution_id, + "authorization": data.authorization, + } + setExecutionRequest(cur_execution) setExecutionModalView(1) setExecutionData(data) + handleUpdateResults(data, cur_execution) }}>
    @@ -6880,7 +6897,17 @@ const AngularWorkflow = (props) => { } return ( -
    +
    { + var currentnode = cy.getElementById(data.action.id) + if (currentnode.length !== 0) { + currentnode.addClass('shuffle-hover-highlight') + } + }} onMouseOut={() => { + var currentnode = cy.getElementById(data.action.id) + if (currentnode.length !== 0) { + currentnode.removeClass('shuffle-hover-highlight') + } + }}>
    { setSelectedResult(data) From d4d704f1700b996b31e7c1a971859ba870e7adf1 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 21 Mar 2021 19:38:16 +0100 Subject: [PATCH 173/185] MAJOR migration fixes for hybrid usage --- backend/go-app/codegen.go | 2004 ------------------------------------- backend/go-app/docker.go | 4 +- backend/go-app/files.go | 882 ---------------- backend/go-app/go.mod | 32 +- backend/go-app/go.sum | 159 +++ backend/go-app/main.go | 645 ++++-------- backend/go-app/oauth2.go | 36 +- backend/go-app/walkoff.go | 1735 ++++++++++++++++---------------- 8 files changed, 1270 insertions(+), 4227 deletions(-) delete mode 100644 backend/go-app/codegen.go delete mode 100644 backend/go-app/files.go diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go deleted file mode 100644 index 5943d4bd..00000000 --- a/backend/go-app/codegen.go +++ /dev/null @@ -1,2004 +0,0 @@ -package main - -import ( - "archive/zip" - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "strconv" - "strings" - - "cloud.google.com/go/storage" - "github.com/getkin/kin-openapi/openapi3" - //"github.com/satori/go.uuid" - "gopkg.in/yaml.v2" -) - -func copyFile(fromfile, tofile string) error { - from, err := os.Open(fromfile) - if err != nil { - return err - } - defer from.Close() - - to, err := os.OpenFile(tofile, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - return err - } - defer to.Close() - - _, err = io.Copy(to, from) - if err != nil { - return err - } - - return nil -} - -func formatAppfile(filedata string) (string, string) { - lines := strings.Split(filedata, "\n") - - newfile := []string{} - classname := "" - for _, line := range lines { - if strings.Contains(line, "walkoff_app_sdk") { - continue - } - - // Remap logging. CBA this right now - // This issue also persists in onprem apps because of await thingies.. :( - // FIXME - if strings.Contains(line, "console_logger") && strings.Contains(line, "await") { - continue - //line = strings.Replace(line, "console_logger", "logger", -1) - //log.Println(line) - } - - // Might not work with different import names - // Could be fucked up with spaces everywhere? Idk - if strings.Contains(line, "class") && strings.Contains(line, "(AppBase)") { - items := strings.Split(line, " ") - if len(items) > 0 && strings.Contains(items[1], "(AppBase)") { - classname = strings.Split(items[1], "(")[0] - } else { - // This could break something.. - classname = "TMP" - } - } - - if strings.Contains(line, "if __name__ ==") { - break - } - - // asyncio.run(HelloWorld.run(), debug=True) - - newfile = append(newfile, line) - } - - filedata = strings.Join(newfile, "\n") - return classname, filedata -} - -// Streams the data into a zip to be used for a cloud function -func streamZipdata(ctx context.Context, identifier, pythoncode, requirements string) (string, error) { - filename := fmt.Sprintf("generated_cloudfunctions/%s.zip", identifier) - - buf := new(bytes.Buffer) - zipWriter := zip.NewWriter(buf) - - zipFile, err := zipWriter.Create("main.py") - if err != nil { - log.Printf("Packing failed to create zip file from bucket: %v", err) - return filename, err - } - - // Have to use Fprintln otherwise it tries to parse all strings etc. - if _, err := fmt.Fprintln(zipFile, pythoncode); err != nil { - return filename, err - } - - zipFile, err = zipWriter.Create("requirements.txt") - if err != nil { - log.Printf("Packing failed to create zip file from bucket: %v", err) - return filename, err - } - if _, err := fmt.Fprintln(zipFile, requirements); err != nil { - return filename, err - } - - err = zipWriter.Close() - if err != nil { - log.Printf("Packing failed to close zip file writer from bucket: %v", err) - return filename, err - } - - return filename, nil -} - -func getAppbase() ([]byte, []byte, error) { - // 1. Have baseline in bucket/generated_apps/baseline - // 2. Copy the baseline to a new folder with identifier name - static := "../app_sdk/static_baseline.py" - appbase := "../app_sdk/app_base.py" - - staticData, err := ioutil.ReadFile(static) - if err != nil { - return []byte{}, []byte{}, err - } - - appbaseData, err := ioutil.ReadFile(appbase) - if err != nil { - return []byte{}, []byte{}, err - } - - return appbaseData, staticData, nil -} - -// Builds the structure for the new generated app in storage (copying baseline files) -func getAppbaseGCP(ctx context.Context, client *storage.Client) ([]byte, []byte, error) { - // 1. Have baseline in bucket/generated_apps/baseline - // 2. Copy the baseline to a new folder with identifier name - basePath := "generated_apps/baseline" - static, err := client.Bucket(bucketName).Object(fmt.Sprintf("%s/static_baseline.py", basePath)).NewReader(ctx) - if err != nil { - return []byte{}, []byte{}, err - } - appbase, err := client.Bucket(bucketName).Object(fmt.Sprintf("%s/app_base.py", basePath)).NewReader(ctx) - if err != nil { - return []byte{}, []byte{}, err - } - - defer static.Close() - defer appbase.Close() - - staticData, err := ioutil.ReadAll(static) - if err != nil { - return []byte{}, []byte{}, err - } - - appbaseData, err := ioutil.ReadAll(appbase) - if err != nil { - return []byte{}, []byte{}, err - } - - return appbaseData, staticData, nil -} - -func fixAppbase(appbase []byte) []string { - record := false - validLines := []string{} - for _, line := range strings.Split(string(appbase), "\n") { - if strings.Contains(line, "#STOPCOPY") { - //log.Println("Stopping copy") - break - } - - if record { - validLines = append(validLines, line) - } - - if strings.Contains(line, "#STARTCOPY") { - //log.Println("Starting copy") - record = true - } - } - - return validLines -} - -// Builds the structure for the new generated app in storage (copying baseline files) -func buildStructureGCP(ctx context.Context, client *storage.Client, swagger *openapi3.Swagger, curHash string) (string, error) { - // 1. Have baseline in bucket/generated_apps/baseline - // 2. Copy the baseline to a new folder with identifier name - - basePath := "generated_apps" - identifier := fmt.Sprintf("%s-%s", swagger.Info.Title, curHash) - appPath := fmt.Sprintf("%s/%s", basePath, identifier) - fileNames := []string{"Dockerfile", "requirements.txt"} - for _, file := range fileNames { - src := client.Bucket(bucketName).Object(fmt.Sprintf("%s/baseline/%s", basePath, file)) - dst := client.Bucket(bucketName).Object(fmt.Sprintf("%s/%s", appPath, file)) - if _, err := dst.CopierFrom(src).Run(ctx); err != nil { - return "", err - } - } - - return appPath, nil -} - -// Builds the base structure for the app that we're making -// Returns error if anything goes wrong. This has to work if -// the python code is supposed to be generated -func buildStructure(swagger *openapi3.Swagger, curHash string) (string, error) { - //log.Printf("%#v", swagger) - - // adding md5 based on input data to not overwrite earlier data. - generatedPath := "generated" - subpath := "../app_gen/openapi/" - identifier := fmt.Sprintf("%s-%s", swagger.Info.Title, curHash) - appPath := fmt.Sprintf("%s/%s", generatedPath, identifier) - - os.MkdirAll(appPath, os.ModePerm) - os.Mkdir(fmt.Sprintf("%s/src", appPath), os.ModePerm) - - err := copyFile(fmt.Sprintf("%sbaseline/Dockerfile", subpath), fmt.Sprintf("%s/%s", appPath, "Dockerfile")) - if err != nil { - log.Println("Failed to move Dockerfile") - return appPath, err - } - - err = copyFile(fmt.Sprintf("%sbaseline/requirements.txt", subpath), fmt.Sprintf("%s/%s", appPath, "requirements.txt")) - if err != nil { - log.Println("Failed to move requrements.txt") - return appPath, err - } - - return appPath, nil -} - -// This function generates the python code that's being used. -// This is really meta when you program it. Handling parameters is hard here. -func makePythoncode(swagger *openapi3.Swagger, name, url, method string, parameters, optionalQueries, headers []string, fileField string) (string, string) { - method = strings.ToLower(method) - queryString := "" - queryData := "" - - // FIXME - this might break - need to check if ? or & should be set as query - parameterData := "" - if len(optionalQueries) > 0 { - queryString += ", " - for index, query := range optionalQueries { - // Check if it's a part of the URL already - queryString += fmt.Sprintf("%s=\"\"", query) - if index != len(optionalQueries)-1 { - queryString += ", " - } - - /* - queryData += fmt.Sprintf(` - if %s: - url += f"&%s={%s}"`, query, query, query) - */ - queryData += fmt.Sprintf(` - if %s: - params["%s"] = %s`, query, query, query) - } - } else { - //log.Printf("No optional queries?") - } - - // api.Authentication.Parameters[0].Value = "BearerAuth" - authenticationParameter := "" - authenticationSetup := "" - authenticationAddin := "" - // Python configuration code that should work :) - if swagger.Components.SecuritySchemes != nil { - if swagger.Components.SecuritySchemes["BearerAuth"] != nil { - authenticationParameter = ", apikey" - authenticationSetup = "if apikey != \" \": headers[\"Authorization\"] = f\"Bearer {apikey}\"" - } else if swagger.Components.SecuritySchemes["BasicAuth"] != nil { - authenticationParameter = ", username_basic, password_basic" - authenticationAddin = ", auth=(username_basic, password_basic)" - } else if swagger.Components.SecuritySchemes["ApiKeyAuth"] != nil { - authenticationParameter = ", apikey" - if swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.In == "header" { - // This is a way to bypass apikeys by passing " " - authenticationSetup = fmt.Sprintf(`if apikey != " ": headers["%s"] = apikey`, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name) - } else if swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.In == "query" { - // This might suck lol - key := "?" - if strings.Contains(url, "?") { - key = "&" - } - - authenticationSetup = fmt.Sprintf("if apikey != \" \": url+=f\"%s%s={apikey}\"", key, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name) - } - } - } - - //baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - // This is a quickfix for onpremises stuff. Does work, but should really be - // part of the authentication scheme from openapi3 - urlParameter := "" - urlInline := "" - //log.Printf("URL: %s", url) - if !strings.HasPrefix(strings.ToLower(url), "http") { - urlParameter = ", url" - urlInline = "{url}" - } - - // Specific check for SSL verification - // This is critical for onprem stuff. - //verifyParam := "" - //verifyWrapper := "" - //verifyAddin := "" - verifyParam := ", ssl_verify=False" - verifyWrapper := `if type(ssl_verify) == str: ssl_verify = False if ssl_verify.lower() == "false" or ssl_verify == "0" else True` - verifyAddin := ", verify=ssl_verify" - - if len(parameters) > 0 { - parameterData = fmt.Sprintf(", %s", strings.Join(parameters, ", ")) - } - - // FIXME - add checks for query data etc - - functionname := strings.ToLower(fmt.Sprintf("%s_%s", method, name)) - if strings.Contains(strings.ToLower(name), strings.ToLower(method)) { - functionname = strings.ToLower(name) - } - - bodyParameter := "" - bodyAddin := "" - bodyFormatter := "" - postParameters := []string{"post", "patch", "put"} - for _, item := range postParameters { - if method == item { - bodyParameter = ", body=\"\"" - bodyAddin = ", data=body" - - // FIXME: Does JSON data work? - bodyFormatter = `body = " ".join(body.strip().split()).encode("utf-8")` - } - } - - preparedHeaders := "headers={}" - if len(headers) > 0 { - preparedHeaders = "headers={" - for count, header := range headers { - headerSplit := strings.Split(header, "=") - added := false - if len(headerSplit) == 2 { - if strings.Contains(preparedHeaders, headerSplit[0]) { - continue - } - - preparedHeaders += fmt.Sprintf(`"%s": "%s"`, headerSplit[0], headerSplit[1]) - added = true - } - - if count != len(headers)-1 && added { - preparedHeaders += "," - } - } - - preparedHeaders += "}" - } - - fileBalance := "" - fileAdder := `` - fileGrabber := `` - fileParameter := `` - if method == "post" && len(fileField) > 0 { - fileParameter = ", file_id" - fileGrabber = `filedata = self.get_file(file_id)` - - // This indentation is confusing (but correct) ROFL - fileAdder = fmt.Sprintf(`if not filedata["success"]: - return file_id+" is not a valid File ID" - files = {"%s": (filedata["filename"], filedata["data"])}`, fileField) - fileBalance = ", files=files" - } - - // Extra param for url if it's changeable - // Extra param for authentication scheme(s) - // The last weird one is the body.. Tabs & spaces sucks. - data := fmt.Sprintf(` async def %s(self%s%s%s%s%s%s%s): - params={} - %s - url=f"%s%s" - %s - %s - %s - %s - %s - %s - ret = requests.%s(url, headers=headers, params=params%s%s%s%s) - try: - return ret.json() - except json.decoder.JSONDecodeError: - return ret.text - `, - functionname, - authenticationParameter, - urlParameter, - fileParameter, - parameterData, - queryString, - bodyParameter, - verifyParam, - preparedHeaders, - urlInline, - url, - verifyWrapper, - authenticationSetup, - queryData, - bodyFormatter, - fileGrabber, - fileAdder, - method, - authenticationAddin, - bodyAddin, - verifyAddin, - fileBalance, - ) - - // Use lowercase when checking - - if strings.Contains(functionname, "attachment") { - //log.Printf("FUNCTION: %s", data) - //log.Println(data) - //log.Printf("Queries: %s", queryString) - } - - return functionname, data -} - -func generateYaml(swagger *openapi3.Swagger, newmd5 string) (*openapi3.Swagger, WorkflowApp, []string, error) { - api := WorkflowApp{} - //log.Printf("%#v", swagger.Info) - - if len(swagger.Info.Title) == 0 { - return swagger, WorkflowApp{}, []string{}, errors.New("Swagger.Info.Title can't be empty.") - } - - if len(swagger.Servers) == 0 { - //return swagger, WorkflowApp{}, []string{}, errors.New("Swagger.Servers can't be empty. Add 'servers':[{'url':'hostname.com'}'") - //return swagger, WorkflowApp{}, []string{}, errors.New("Swagger.Servers can't be empty. Add 'servers':[{'url':'hostname.com'}'") - swagger.Servers = openapi3.Servers{ - &openapi3.Server{ - URL: "https://hostname.com", - }, - } - } - - api.Name = swagger.Info.Title - api.Description = swagger.Info.Description - - // FIXME: Versioning issue? - api.ID = newmd5 - //uuid.NewV4().String() - - api.IsValid = true - api.Link = swagger.Servers[0].URL // host does not exist lol - if strings.HasSuffix(api.Link, "/") { - api.Link = api.Link[:len(api.Link)-1] - } - - api.AppVersion = "1.0.0" - api.Environment = "Shuffle" - api.SmallImage = "" - api.LargeImage = "" - api.Sharing = false - api.Verified = false - api.Tested = false - api.Invalid = false - api.PrivateID = newmd5 - api.Generated = true - api.Activated = true - // Setting up security schemes - extraParameters := []WorkflowAppActionParameter{} - - if val, ok := swagger.Info.ExtensionProps.Extensions["x-logo"]; ok { - j, err := json.Marshal(&val) - if err == nil { - if j[0] == 0x22 && j[len(j)-1] == 0x22 { - j = j[1 : len(j)-1] - } - - //log.Printf("%s", j) - api.SmallImage = string(j) - api.LargeImage = string(j) - } - } - - // Jesus what a clusterfuck. - // Handles parsing of categories from OpenApi3 custom field - if val, ok := swagger.Info.ExtensionProps.Extensions["x-categories"]; ok { - //log.Printf("Categories: %#v", val) - j, err := json.Marshal(&val) - if err == nil { - if j[0] == 0x22 && j[len(j)-1] == 0x22 { - j = j[1 : len(j)-1] - } - - parsedCategories := fmt.Sprintf(`{"categories": %s}`, string(j)) - type parsed struct { - Categories []string `json:"categories"` - } - - var parse parsed - err := json.Unmarshal([]byte(parsedCategories), &parse) - if err != nil { - log.Printf("Failed unmarshaling categories: %v", err) - } else { - api.Categories = parse.Categories - } - } - } - - if len(swagger.Tags) > 0 { - newTags := []string{} - for _, tag := range swagger.Tags { - newTags = append(newTags, tag.Name) - } - - api.Tags = newTags - } - - securitySchemes := swagger.Components.SecuritySchemes - if securitySchemes != nil { - //log.Printf("%#v", securitySchemes) - - api.Authentication = Authentication{ - Required: true, - Parameters: []AuthenticationParams{}, - } - - // Used for python code generation lol - // Not sure how this should work with oauth - if securitySchemes["BearerAuth"] != nil { - api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ - Name: "apikey", - Value: "", - Example: "******", - Description: securitySchemes["BearerAuth"].Value.Description, - In: securitySchemes["BearerAuth"].Value.In, - Scheme: securitySchemes["BearerAuth"].Value.Scheme, - Schema: SchemaDefinition{ - Type: securitySchemes["BearerAuth"].Value.Scheme, - }, - }) - - //log.Printf("HANDLE BEARER AUTH") - extraParameters = append(extraParameters, WorkflowAppActionParameter{ - Name: "apikey", - Description: "The apikey to use", - Multiline: false, - Required: true, - Example: "The API key to use. Space = skip", - Configuration: true, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - } else if securitySchemes["ApiKeyAuth"] != nil { - api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ - Name: "apikey", - Value: "", - Example: "******", - Description: securitySchemes["ApiKeyAuth"].Value.Description, - In: securitySchemes["ApiKeyAuth"].Value.In, - Scheme: securitySchemes["ApiKeyAuth"].Value.Scheme, - Schema: SchemaDefinition{ - Type: securitySchemes["ApiKeyAuth"].Value.Scheme, - }, - }) - - //log.Printf("HANDLE APIKEY AUTH") - extraParameters = append(extraParameters, WorkflowAppActionParameter{ - Name: "apikey", - Description: "The apikey to use", - Multiline: false, - Required: true, - Example: "**********", - Configuration: true, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - } else if securitySchemes["BasicAuth"] != nil { - api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ - Name: "username_basic", - Value: "", - Example: "username", - Description: securitySchemes["BasicAuth"].Value.Description, - In: securitySchemes["BasicAuth"].Value.In, - Scheme: securitySchemes["BasicAuth"].Value.Scheme, - Schema: SchemaDefinition{ - Type: securitySchemes["BasicAuth"].Value.Scheme, - }, - }) - - api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ - Name: "password_basic", - Value: "", - Example: "*****", - Description: securitySchemes["BasicAuth"].Value.Description, - In: securitySchemes["BasicAuth"].Value.In, - Scheme: securitySchemes["BasicAuth"].Value.Scheme, - Schema: SchemaDefinition{ - Type: securitySchemes["BasicAuth"].Value.Scheme, - }, - }) - - extraParameters = append(extraParameters, WorkflowAppActionParameter{ - Name: "username_basic", - Description: "The username to use", - Multiline: false, - Required: true, - Example: "The username to use", - Configuration: true, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - extraParameters = append(extraParameters, WorkflowAppActionParameter{ - Name: "password_basic", - Description: "The password to use", - Multiline: false, - Required: true, - Example: "***********", - Configuration: true, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - } - } - - // Adds a link parameter if it's not already defined - if len(api.Link) == 0 { - api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ - Name: "url", - Description: "The URL of the app", - Multiline: false, - Required: true, - Example: "https://shuffler.io", - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - extraParameters = append(extraParameters, WorkflowAppActionParameter{ - Name: "url", - Description: "The URL of the app", - Multiline: false, - Required: true, - Configuration: true, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - } - - // This is the python code to be generated - // Could just as well be go at this point lol - pythonFunctions := []string{} - //Verified bool `json:"verified" yaml:"verified" required:false datastore:"verified"` - for actualPath, path := range swagger.Paths { - actualPath = strings.Replace(actualPath, " ", "_", -1) - //actualPath = strings.Replace(actualPath, ".", "", -1) - actualPath = strings.Replace(actualPath, "\\", "", -1) - if !api.Invalid && strings.HasPrefix(actualPath, "tmp") { - log.Printf("[WARNING] Set api %s to invalid because of path %s", swagger.Info.Title, actualPath) - api.Invalid = true - } - - // FIXME: Handle everything behind questionmark (?) with dots as well. - // https://godoc.org/github.com/getkin/kin-openapi/openapi3#PathItem - if path.Get != nil { - action, curCode := handleGet(swagger, api, extraParameters, path, actualPath) - api.Actions = append(api.Actions, action) - pythonFunctions = append(pythonFunctions, curCode) - } - if path.Connect != nil { - action, curCode := handleConnect(swagger, api, extraParameters, path, actualPath) - api.Actions = append(api.Actions, action) - pythonFunctions = append(pythonFunctions, curCode) - } - if path.Head != nil { - action, curCode := handleHead(swagger, api, extraParameters, path, actualPath) - api.Actions = append(api.Actions, action) - pythonFunctions = append(pythonFunctions, curCode) - } - if path.Delete != nil { - action, curCode := handleDelete(swagger, api, extraParameters, path, actualPath) - api.Actions = append(api.Actions, action) - pythonFunctions = append(pythonFunctions, curCode) - } - if path.Post != nil { - action, curCode := handlePost(swagger, api, extraParameters, path, actualPath) - api.Actions = append(api.Actions, action) - pythonFunctions = append(pythonFunctions, curCode) - } - if path.Patch != nil { - action, curCode := handlePatch(swagger, api, extraParameters, path, actualPath) - api.Actions = append(api.Actions, action) - pythonFunctions = append(pythonFunctions, curCode) - } - if path.Put != nil { - action, curCode := handlePut(swagger, api, extraParameters, path, actualPath) - api.Actions = append(api.Actions, action) - pythonFunctions = append(pythonFunctions, curCode) - } - - // Has to be here because its used differently above. - // FIXING this is done during export instead? - //log.Printf("OLDPATH: %s", actualPath) - //if strings.Contains(actualPath, "?") { - // actualPath = strings.Split(actualPath, "?")[0] - //} - - //log.Printf("NEWPATH: %s", actualPath) - //newPaths[actualPath] = path - } - - return swagger, api, pythonFunctions, nil -} - -// FIXME - have this give a real version? -func verifyApi(api WorkflowApp) WorkflowApp { - if api.AppVersion == "" { - api.AppVersion = "1.0.0" - } - - return api -} - -func getBasePython() string { - baseString := `import requests -import asyncio -import json -import urllib3 - -from walkoff_app_sdk.app_base import AppBase - -class %s(AppBase): - """ - Autogenerated class by Shuffler - """ - - __version__ = "%s" - app_name = "%s" - - def __init__(self, redis, logger, console_logger=None): - self.verify = False - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - super().__init__(redis, logger, console_logger) - -%s - -if __name__ == "__main__": - asyncio.run(%s.run(), debug=True) -` - return baseString -} - -func dumpPythonGCP(ctx context.Context, client *storage.Client, basePath, name, version string, pythonFunctions []string) (string, error) { - parsedCode := fmt.Sprintf(getBasePython(), name, version, name, strings.Join(pythonFunctions, "\n"), name) - - // Create bucket handle - bucket := client.Bucket(bucketName) - obj := bucket.Object(fmt.Sprintf("%s/src/app.py", basePath)) - w := obj.NewWriter(ctx) - if _, err := fmt.Fprintf(w, parsedCode); err != nil { - return "", err - } - // Close, just like writing a file. - if err := w.Close(); err != nil { - return "", err - } - - return parsedCode, nil -} - -func dumpPython(basePath, name, version string, pythonFunctions []string) (string, error) { - //log.Printf("%#v", api) - //log.Printf(strings.Join(pythonFunctions, "\n")) - - parsedCode := fmt.Sprintf(getBasePython(), name, version, name, strings.Join(pythonFunctions, "\n"), name) - - err := ioutil.WriteFile(fmt.Sprintf("%s/src/app.py", basePath), []byte(parsedCode), os.ModePerm) - if err != nil { - return "", err - } - //fmt.Println(parsedCode) - //log.Println(string(data)) - return parsedCode, nil -} - -func dumpApiGCP(ctx context.Context, client *storage.Client, swagger *openapi3.Swagger, basePath string, api WorkflowApp) error { - //log.Printf("%#v", api) - data, err := yaml.Marshal(api) - if err != nil { - log.Printf("Error with yaml marshal: %s", err) - return err - } - - // Create bucket handle - bucket := client.Bucket(bucketName) - obj := bucket.Object(fmt.Sprintf("%s/app.yaml", basePath)) - w := obj.NewWriter(ctx) - if _, err := fmt.Fprintln(w, string(data)); err != nil { - return err - } - // Close, just like writing a file. - if err := w.Close(); err != nil { - return err - } - - openapidata, err := yaml.Marshal(swagger) - if err != nil { - log.Printf("Error with yaml marshal: %s", err) - return err - } - obj = bucket.Object(fmt.Sprintf("%s/openapi.yaml", basePath)) - //log.Println(string(openapidata)) - w = obj.NewWriter(ctx) - if _, err := fmt.Fprintln(w, string(openapidata)); err != nil { - return err - } - // Close, just like writing a file. - if err := w.Close(); err != nil { - return err - } - - //log.Println(string(data)) - return nil -} - -func dumpApi(basePath string, api WorkflowApp) error { - //log.Printf("%#v", api) - data, err := yaml.Marshal(api) - if err != nil { - log.Printf("Error with yaml marshal: %s", err) - return err - } - - err = ioutil.WriteFile(fmt.Sprintf("%s/api.yaml", basePath), []byte(data), os.ModePerm) - if err != nil { - return err - } - - //log.Println(string(data)) - return nil -} - -func getRunner(classname string) string { - return fmt.Sprintf(` -# Run the actual thing after we've checked params -def run(request): - print("Started execution!") - action = request.get_json() - print(action) - print(type(action)) - authorization_key = action.get("authorization") - current_execution_id = action.get("execution_id") - - if action and "name" in action and "app_name" in action: - asyncio.run(%s.run(action), debug=True) - return f'Attempting to execute function {action["name"]} in app {action["app_name"]}' - else: - return f'Invalid action' - - `, classname) -} - -func deployAppToDatastore(ctx context.Context, workflowapp WorkflowApp) error { - err := setWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) - if err != nil { - log.Printf("[ERROR] Failed setting workflowapp: %s", err) - return err - } else { - log.Printf("[INFO] Added %s:%s to the database", workflowapp.Name, workflowapp.AppVersion) - } - - return nil -} - -// FIXME: -// https://docs.python.org/3.2/reference/lexical_analysis.html#identifiers -// This is used to build the python functions. -func fixFunctionName(functionName, actualPath string) string { - if len(functionName) == 0 { - functionName = actualPath - } - - // REGEX THIS SHIT - // ROFL - - //log.Printf("Fixing function name for %s", functionName) - functionName = strings.Replace(functionName, ".", "", -1) - functionName = strings.Replace(functionName, ",", "", -1) - functionName = strings.Replace(functionName, ".", "", -1) - functionName = strings.Replace(functionName, "&", "", -1) - functionName = strings.Replace(functionName, "/", "", -1) - functionName = strings.Replace(functionName, "\\", "", -1) - - functionName = strings.Replace(functionName, "!", "", -1) - functionName = strings.Replace(functionName, "?", "", -1) - functionName = strings.Replace(functionName, "@", "", -1) - functionName = strings.Replace(functionName, "#", "", -1) - functionName = strings.Replace(functionName, "$", "", -1) - functionName = strings.Replace(functionName, "&", "", -1) - functionName = strings.Replace(functionName, "*", "", -1) - functionName = strings.Replace(functionName, "(", "", -1) - functionName = strings.Replace(functionName, ")", "", -1) - functionName = strings.Replace(functionName, "[", "", -1) - functionName = strings.Replace(functionName, "]", "", -1) - functionName = strings.Replace(functionName, "{", "", -1) - functionName = strings.Replace(functionName, "}", "", -1) - functionName = strings.Replace(functionName, `"`, "", -1) - functionName = strings.Replace(functionName, `'`, "", -1) - functionName = strings.Replace(functionName, `|`, "", -1) - functionName = strings.Replace(functionName, `~`, "", -1) - - functionName = strings.Replace(functionName, " ", "_", -1) - functionName = strings.Replace(functionName, "-", "_", -1) - - functionName = strings.ToLower(functionName) - - return functionName -} - -// Returns a valid param name -func validateParameterName(name string) string { - invalid := []string{"False", - "await", - "else", - "import", - "pass", - "None", - "break", - "except", - "in", - "raise", - "True", - "class", - "finally", - "is", - "return", - "and", - "continue", - "for", - "lambda", - "try", - "as", - "def", - "from", - "nonlocal", - "while", - "assert", - "del", - "global", - "not", - "with", - "async", - "elif", - "if", - "or", - "yield", - } - - newname := name - for _, item := range invalid { - if item == name { - //log.Printf("%s is NOT a valid parameter name!", item) - newname = fmt.Sprintf("%s_shuffle", item) - break - } - } - - newname = strings.ReplaceAll(newname, " ", "_") - newname = strings.ReplaceAll(newname, ",", "_") - newname = strings.ReplaceAll(newname, ".", "_") - newname = strings.ReplaceAll(newname, "|", "_") - newname = strings.ReplaceAll(newname, "-", "_") - - return newname -} - -func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string) (WorkflowAppAction, string) { - // What to do with this, hmm - functionName := fixFunctionName(path.Connect.Summary, actualPath) - - action := WorkflowAppAction{ - Description: path.Connect.Description, - Name: fmt.Sprintf("%s %s", "Connect", path.Connect.Summary), - Label: fmt.Sprintf(path.Connect.Summary), - NodeType: "action", - Environment: api.Environment, - Parameters: extraParameters, - } - - action.Returns.Schema.Type = "string" - baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - - //log.Println(path.Parameters) - - // Parameters: []WorkflowAppActionParameter{}, - // FIXME - add data for POST stuff - firstQuery := true - optionalQueries := []string{} - parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} - - headersFound := []string{} - if len(path.Connect.Parameters) > 0 { - for counter, param := range path.Connect.Parameters { - if param.Value.Schema == nil { - continue - } else if param.Value.In == "header" { - headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) - continue - } - - parsedName := param.Value.Name - parsedName = strings.ReplaceAll(parsedName, " ", "_") - parsedName = strings.ReplaceAll(parsedName, ",", "_") - parsedName = strings.ReplaceAll(parsedName, ".", "_") - parsedName = strings.ReplaceAll(parsedName, "|", "_") - parsedName = strings.ReplaceAll(parsedName, "-", "_") - parsedName = validateParameterName(parsedName) - param.Value.Name = parsedName - path.Connect.Parameters[counter].Value.Name = parsedName - - curParam := WorkflowAppActionParameter{ - Name: parsedName, - Description: param.Value.Description, - Multiline: false, - Required: param.Value.Required, - Schema: SchemaDefinition{ - Type: param.Value.Schema.Value.Type, - }, - } - - // FIXME: Example & Multiline - if param.Value.Example != nil { - curParam.Example = param.Value.Example.(string) - - if param.Value.Name == "body" { - curParam.Value = param.Value.Example.(string) - } - } - if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { - j, err := json.Marshal(&val) - if err == nil { - b, err := strconv.ParseBool(string(j)) - if err == nil { - curParam.Multiline = b - } - } - } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - - if param.Value.In == "path" { - parameters = append(parameters, curParam.Name) - //baseUrl = fmt.Sprintf("%s%s", baseUrl) - } else if param.Value.In == "query" { - //log.Printf("QUERY!: %s", param.Value.Name) - if !param.Value.Required { - optionalQueries = append(optionalQueries, param.Value.Name) - continue - } - - parameters = append(parameters, param.Value.Name) - - if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { - continue - } - - if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { - continue - } - - if firstQuery && !strings.Contains(baseUrl, "?") { - baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } else { - baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } - firstQuery = false - } - - } - } - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Options: []string{ - "True", - "False", - }, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - // ensuring that they end up last in the specification - // (order is ish important for optional params) - they need to be last. - for _, optionalParam := range optionalParameters { - action.Parameters = append(action.Parameters, optionalParam) - } - - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "connect", parameters, optionalQueries, headersFound, "") - - if len(functionname) > 0 { - action.Name = functionname - } - - return action, curCode -} - -func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string) (WorkflowAppAction, string) { - // What to do with this, hmm - functionName := fixFunctionName(path.Get.Summary, actualPath) - - action := WorkflowAppAction{ - Description: path.Get.Description, - Name: fmt.Sprintf("%s %s", "Get", path.Get.Summary), - Label: fmt.Sprintf(path.Get.Summary), - NodeType: "action", - Environment: api.Environment, - Parameters: extraParameters, - } - - action.Returns.Schema.Type = "string" - baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - - // Parameters: []WorkflowAppActionParameter{}, - // FIXME - add data for POST stuff - firstQuery := true - optionalQueries := []string{} - - // FIXME - remove this when authentication is properly introduced - parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} - - headersFound := []string{} - if len(path.Get.Parameters) > 0 { - for counter, param := range path.Get.Parameters { - if param.Value.Schema == nil { - continue - } else if param.Value.In == "header" { - headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) - continue - } - - parsedName := param.Value.Name - parsedName = strings.ReplaceAll(parsedName, " ", "_") - parsedName = strings.ReplaceAll(parsedName, ",", "_") - parsedName = strings.ReplaceAll(parsedName, ".", "_") - parsedName = strings.ReplaceAll(parsedName, "|", "_") - parsedName = validateParameterName(parsedName) - param.Value.Name = parsedName - path.Get.Parameters[counter].Value.Name = parsedName - - curParam := WorkflowAppActionParameter{ - Name: parsedName, - Description: param.Value.Description, - Multiline: false, - Required: param.Value.Required, - Schema: SchemaDefinition{ - Type: param.Value.Schema.Value.Type, - }, - } - - // FIXME: Example & Multiline - if param.Value.Example != nil { - curParam.Example = param.Value.Example.(string) - - if param.Value.Name == "body" { - curParam.Value = param.Value.Example.(string) - } - } - if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { - j, err := json.Marshal(&val) - if err == nil { - b, err := strconv.ParseBool(string(j)) - if err == nil { - curParam.Multiline = b - } - } - } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - - if param.Value.In == "path" { - parameters = append(parameters, curParam.Name) - //baseUrl = fmt.Sprintf("%s%s", baseUrl) - } else if param.Value.In == "query" { - //log.Printf("QUERY!: %s", param.Value.Name) - if !param.Value.Required { - optionalQueries = append(optionalQueries, param.Value.Name) - continue - } - - parameters = append(parameters, param.Value.Name) - - // Skipping simial - if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { - continue - } - - if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { - continue - } - - if firstQuery && !strings.Contains(baseUrl, "?") { - baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } else { - baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } - firstQuery = false - } - - } - } - - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Options: []string{ - "True", - "False", - }, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - // ensuring that they end up last in the specification - // (order is ish important for optional params) - they need to be last. - for _, optionalParam := range optionalParameters { - action.Parameters = append(action.Parameters, optionalParam) - } - - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "get", parameters, optionalQueries, headersFound, "") - - if len(functionname) > 0 { - action.Name = functionname - } - - return action, curCode -} - -func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string) (WorkflowAppAction, string) { - // What to do with this, hmm - functionName := fixFunctionName(path.Head.Summary, actualPath) - - action := WorkflowAppAction{ - Description: path.Head.Description, - Name: fmt.Sprintf("%s %s", "Head", path.Head.Summary), - Label: fmt.Sprintf(path.Head.Summary), - NodeType: "action", - Environment: api.Environment, - Parameters: extraParameters, - } - - action.Returns.Schema.Type = "string" - baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - - //log.Println(path.Parameters) - - // Parameters: []WorkflowAppActionParameter{}, - // FIXME - add data for POST stuff - firstQuery := true - optionalQueries := []string{} - parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} - - headersFound := []string{} - if len(path.Head.Parameters) > 0 { - for counter, param := range path.Head.Parameters { - if param.Value.Schema == nil { - continue - } else if param.Value.In == "header" { - headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) - continue - } - - parsedName := param.Value.Name - parsedName = strings.ReplaceAll(parsedName, " ", "_") - parsedName = strings.ReplaceAll(parsedName, ",", "_") - parsedName = strings.ReplaceAll(parsedName, ".", "_") - parsedName = strings.ReplaceAll(parsedName, "|", "_") - parsedName = validateParameterName(parsedName) - param.Value.Name = parsedName - path.Head.Parameters[counter].Value.Name = parsedName - - curParam := WorkflowAppActionParameter{ - Name: parsedName, - Description: param.Value.Description, - Multiline: false, - Required: param.Value.Required, - Schema: SchemaDefinition{ - Type: param.Value.Schema.Value.Type, - }, - } - - // FIXME: Example & Multiline - if param.Value.Example != nil { - curParam.Example = param.Value.Example.(string) - - if param.Value.Name == "body" { - curParam.Value = param.Value.Example.(string) - } - } - if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { - j, err := json.Marshal(&val) - if err == nil { - b, err := strconv.ParseBool(string(j)) - if err == nil { - curParam.Multiline = b - } - } - } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - - if param.Value.In == "path" { - parameters = append(parameters, curParam.Name) - //baseUrl = fmt.Sprintf("%s%s", baseUrl) - } else if param.Value.In == "query" { - //log.Printf("QUERY!: %s", param.Value.Name) - if !param.Value.Required { - optionalQueries = append(optionalQueries, param.Value.Name) - continue - } - - parameters = append(parameters, param.Value.Name) - - if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { - continue - } - - if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { - continue - } - - if firstQuery && !strings.Contains(baseUrl, "?") { - baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } else { - baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } - firstQuery = false - } - } - } - - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Options: []string{ - "True", - "False", - }, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - // ensuring that they end up last in the specification - // (order is ish important for optional params) - they need to be last. - for _, optionalParam := range optionalParameters { - action.Parameters = append(action.Parameters, optionalParam) - } - - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "head", parameters, optionalQueries, headersFound, "") - - if len(functionname) > 0 { - action.Name = functionname - } - - return action, curCode -} - -func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string) (WorkflowAppAction, string) { - // What to do with this, hmm - functionName := fixFunctionName(path.Delete.Summary, actualPath) - - action := WorkflowAppAction{ - Description: path.Delete.Description, - Name: fmt.Sprintf("%s %s", "Delete", path.Delete.Summary), - Label: fmt.Sprintf(path.Delete.Summary), - NodeType: "action", - Environment: api.Environment, - Parameters: extraParameters, - } - - action.Returns.Schema.Type = "string" - baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - - //log.Println(path.Parameters) - - // Parameters: []WorkflowAppActionParameter{}, - // FIXME - add data for POST stuff - firstQuery := true - optionalQueries := []string{} - parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} - - headersFound := []string{} - if len(path.Delete.Parameters) > 0 { - for counter, param := range path.Delete.Parameters { - if param.Value.Schema == nil { - continue - } else if param.Value.In == "header" { - headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) - continue - } - - parsedName := param.Value.Name - parsedName = strings.ReplaceAll(parsedName, " ", "_") - parsedName = strings.ReplaceAll(parsedName, ",", "_") - parsedName = strings.ReplaceAll(parsedName, ".", "_") - parsedName = strings.ReplaceAll(parsedName, "|", "_") - parsedName = validateParameterName(parsedName) - param.Value.Name = parsedName - path.Delete.Parameters[counter].Value.Name = parsedName - - curParam := WorkflowAppActionParameter{ - Name: parsedName, - Description: param.Value.Description, - Multiline: false, - Required: param.Value.Required, - Schema: SchemaDefinition{ - Type: param.Value.Schema.Value.Type, - }, - } - - // FIXME: Example & Multiline - if param.Value.Example != nil { - curParam.Example = param.Value.Example.(string) - - if param.Value.Name == "body" { - curParam.Value = param.Value.Example.(string) - } - } - if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { - j, err := json.Marshal(&val) - if err == nil { - b, err := strconv.ParseBool(string(j)) - if err == nil { - curParam.Multiline = b - } - } - } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - - if param.Value.In == "path" { - parameters = append(parameters, curParam.Name) - //baseUrl = fmt.Sprintf("%s%s", baseUrl) - } else if param.Value.In == "query" { - //log.Printf("QUERY!: %s", param.Value.Name) - if !param.Value.Required { - optionalQueries = append(optionalQueries, param.Value.Name) - continue - } - - parameters = append(parameters, param.Value.Name) - - if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { - continue - } - - if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { - continue - } - - if firstQuery && !strings.Contains(baseUrl, "?") { - baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } else { - baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } - firstQuery = false - } - - } - } - - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Options: []string{ - "True", - "False", - }, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - // ensuring that they end up last in the specification - // (order is ish important for optional params) - they need to be last. - for _, optionalParam := range optionalParameters { - action.Parameters = append(action.Parameters, optionalParam) - } - - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "delete", parameters, optionalQueries, headersFound, "") - - if len(functionname) > 0 { - action.Name = functionname - } - - return action, curCode -} - -func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string) (WorkflowAppAction, string) { - // What to do with this, hmm - //log.Printf("PATH: %s", actualPath) - functionName := fixFunctionName(path.Post.Summary, actualPath) - - action := WorkflowAppAction{ - Description: path.Post.Description, - Name: fmt.Sprintf("%s %s", "Post", path.Post.Summary), - Label: fmt.Sprintf(path.Post.Summary), - NodeType: "action", - Environment: api.Environment, - Parameters: extraParameters, - } - - action.Returns.Schema.Type = "string" - baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - - // Parameters: []WorkflowAppActionParameter{}, - // FIXME - add data for POST stuff - firstQuery := true - optionalQueries := []string{} - parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} - - fileField := "" - if path.Post.RequestBody != nil { - //log.Printf("DATA: %#v", - value := path.Post.RequestBody.Value - //log.Printf("VAL: %#v", value.Content) - if val, ok := value.Content["multipart/form-data"]; ok { - if val.Schema.Value != nil { - if innerval, ok := val.Schema.Value.Properties["fieldname"]; ok { - if extensionvalue, ok := innerval.Value.ExtensionProps.Extensions["value"]; ok { - fieldname := extensionvalue.(json.RawMessage) - newName := string(fmt.Sprintf("%s", string(fieldname))) - if newName[0] == 0x22 && newName[len(newName)-1] == 0x22 { - parsedName := newName[1 : len(newName)-1] - //log.Printf("[INFO] Parse name: %s", parsedName) - fileField = parsedName - - curParam := WorkflowAppActionParameter{ - Name: "file_id", - Description: "Files to be uploaded", - Multiline: false, - Required: true, - Schema: SchemaDefinition{ - Type: "string", - }, - } - - action.Parameters = append(action.Parameters, curParam) - } - } - } - } - } - } - - headersFound := []string{} - if len(path.Post.Parameters) > 0 { - for counter, param := range path.Post.Parameters { - if param.Value.Schema == nil { - continue - } else if param.Value.In == "header" { - headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) - continue - } - - parsedName := param.Value.Name - parsedName = strings.ReplaceAll(parsedName, " ", "_") - parsedName = strings.ReplaceAll(parsedName, ",", "_") - parsedName = strings.ReplaceAll(parsedName, ".", "_") - parsedName = strings.ReplaceAll(parsedName, "|", "_") - parsedName = validateParameterName(parsedName) - param.Value.Name = parsedName - path.Post.Parameters[counter].Value.Name = parsedName - - curParam := WorkflowAppActionParameter{ - Name: parsedName, - Description: param.Value.Description, - Multiline: false, - Required: param.Value.Required, - Schema: SchemaDefinition{ - Type: param.Value.Schema.Value.Type, - }, - } - - // FIXME: Example & Multiline - if param.Value.Example != nil { - curParam.Example = param.Value.Example.(string) - - if parsedName == "body" { - curParam.Value = param.Value.Example.(string) - } - } - if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { - j, err := json.Marshal(&val) - if err == nil { - b, err := strconv.ParseBool(string(j)) - if err == nil { - curParam.Multiline = b - } - } - } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - - if param.Value.In == "path" { - parameters = append(parameters, curParam.Name) - //baseUrl = fmt.Sprintf("%s%s", baseUrl) - } else if param.Value.In == "query" { - //log.Printf("QUERY!: %s", param.Value.Name) - if !param.Value.Required { - optionalQueries = append(optionalQueries, param.Value.Name) - continue - } - - parameters = append(parameters, param.Value.Name) - - if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { - continue - } - - if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { - continue - } - - if firstQuery && !strings.Contains(baseUrl, "?") { - baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } else { - baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } - firstQuery = false - } - } - } - - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Options: []string{ - "True", - "False", - }, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - // ensuring that they end up last in the specification - // (order is ish important for optional params) - they need to be last. - for _, optionalParam := range optionalParameters { - action.Parameters = append(action.Parameters, optionalParam) - } - - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "post", parameters, optionalQueries, headersFound, fileField) - - if len(functionname) > 0 { - action.Name = functionname - } - - //log.Printf("PARAMS: %d", len(action.Parameters)) - //for _, param := range action.Parameters { - // log.Printf("%#v", param) - //} - - return action, curCode -} - -func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string) (WorkflowAppAction, string) { - // What to do with this, hmm - functionName := fixFunctionName(path.Patch.Summary, actualPath) - - action := WorkflowAppAction{ - Description: path.Patch.Description, - Name: fmt.Sprintf("%s %s", "Patch", path.Patch.Summary), - Label: fmt.Sprintf(path.Patch.Summary), - NodeType: "action", - Environment: api.Environment, - Parameters: extraParameters, - } - - action.Returns.Schema.Type = "string" - baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - - //log.Println(path.Parameters) - - // Parameters: []WorkflowAppActionParameter{}, - // FIXME - add data for POST stuff - firstQuery := true - optionalQueries := []string{} - parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} - - headersFound := []string{} - if len(path.Patch.Parameters) > 0 { - for counter, param := range path.Patch.Parameters { - if param.Value.Schema == nil { - continue - } else if param.Value.In == "header" { - headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) - continue - } - - parsedName := param.Value.Name - parsedName = strings.ReplaceAll(parsedName, " ", "_") - parsedName = strings.ReplaceAll(parsedName, ",", "_") - parsedName = strings.ReplaceAll(parsedName, ".", "_") - parsedName = strings.ReplaceAll(parsedName, "|", "_") - parsedName = validateParameterName(parsedName) - param.Value.Name = parsedName - path.Patch.Parameters[counter].Value.Name = parsedName - - curParam := WorkflowAppActionParameter{ - Name: parsedName, - Description: param.Value.Description, - Multiline: false, - Required: param.Value.Required, - Schema: SchemaDefinition{ - Type: param.Value.Schema.Value.Type, - }, - } - - // FIXME: Example & Multiline - if param.Value.Example != nil { - curParam.Example = param.Value.Example.(string) - - if param.Value.Name == "body" { - curParam.Value = param.Value.Example.(string) - } - } - if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { - j, err := json.Marshal(&val) - if err == nil { - b, err := strconv.ParseBool(string(j)) - if err == nil { - curParam.Multiline = b - } - } - } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - - if param.Value.In == "path" { - parameters = append(parameters, curParam.Name) - //baseUrl = fmt.Sprintf("%s%s", baseUrl) - } else if param.Value.In == "query" { - //log.Printf("QUERY!: %s", param.Value.Name) - if !param.Value.Required { - optionalQueries = append(optionalQueries, param.Value.Name) - continue - } - - parameters = append(parameters, param.Value.Name) - - if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { - continue - } - - if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { - continue - } - - if firstQuery && !strings.Contains(baseUrl, "?") { - baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } else { - baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } - firstQuery = false - } - } - } - - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Options: []string{ - "True", - "False", - }, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - // ensuring that they end up last in the specification - // (order is ish important for optional params) - they need to be last. - for _, optionalParam := range optionalParameters { - action.Parameters = append(action.Parameters, optionalParam) - } - - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "patch", parameters, optionalQueries, headersFound, "") - - if len(functionname) > 0 { - action.Name = functionname - } - - return action, curCode -} - -func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string) (WorkflowAppAction, string) { - // What to do with this, hmm - functionName := fixFunctionName(path.Put.Summary, actualPath) - - action := WorkflowAppAction{ - Description: path.Put.Description, - Name: fmt.Sprintf("%s %s", "Put", path.Put.Summary), - Label: fmt.Sprintf(path.Put.Summary), - NodeType: "action", - Environment: api.Environment, - Parameters: extraParameters, - } - - action.Returns.Schema.Type = "string" - baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - - //log.Println(path.Parameters) - - // Parameters: []WorkflowAppActionParameter{}, - // FIXME - add data for POST stuff - firstQuery := true - optionalQueries := []string{} - parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} - - headersFound := []string{} - if len(path.Put.Parameters) > 0 { - for counter, param := range path.Put.Parameters { - if param.Value.Schema == nil { - continue - } else if param.Value.In == "header" { - headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) - continue - } - - parsedName := param.Value.Name - parsedName = strings.ReplaceAll(parsedName, " ", "_") - parsedName = strings.ReplaceAll(parsedName, ",", "_") - parsedName = strings.ReplaceAll(parsedName, ".", "_") - parsedName = strings.ReplaceAll(parsedName, "|", "_") - parsedName = validateParameterName(parsedName) - param.Value.Name = parsedName - path.Put.Parameters[counter].Value.Name = parsedName - - curParam := WorkflowAppActionParameter{ - Name: parsedName, - Description: param.Value.Description, - Multiline: false, - Required: param.Value.Required, - Schema: SchemaDefinition{ - Type: param.Value.Schema.Value.Type, - }, - } - - // FIXME: Example & Multiline - if param.Value.Example != nil { - curParam.Example = param.Value.Example.(string) - - if param.Value.Name == "body" { - curParam.Value = param.Value.Example.(string) - } - } - if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { - j, err := json.Marshal(&val) - if err == nil { - b, err := strconv.ParseBool(string(j)) - if err == nil { - curParam.Multiline = b - } - } - } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - - if param.Value.In == "path" { - parameters = append(parameters, param.Value.Name) - //baseUrl = fmt.Sprintf("%s%s", baseUrl) - } else if param.Value.In == "query" { - //log.Printf("QUERY!: %s", param.Value.Name) - if !param.Value.Required { - optionalQueries = append(optionalQueries, param.Value.Name) - continue - } - - parameters = append(parameters, param.Value.Name) - - if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { - continue - } - - if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { - continue - } - - if firstQuery && !strings.Contains(baseUrl, "?") { - baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } else { - baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) - } - firstQuery = false - } - - } - } - - optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ - Name: "ssl_verify", - Description: "Check if you want to verify request", - Multiline: false, - Required: false, - Example: "True", - Options: []string{ - "True", - "False", - }, - Schema: SchemaDefinition{ - Type: "string", - }, - }) - - // ensuring that they end up last in the specification - // (order is ish important for optional params) - they need to be last. - for _, optionalParam := range optionalParameters { - action.Parameters = append(action.Parameters, optionalParam) - } - - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "put", parameters, optionalQueries, headersFound, "") - - if len(functionname) > 0 { - action.Name = functionname - } - - return action, curCode -} diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 27c8e436..e11934f8 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -2,6 +2,8 @@ package main // Docker import ( + "github.com/frikky/shuffle-shared" + "archive/tar" "path/filepath" @@ -797,7 +799,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) { } // Just here to verify that the user is logged in - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in validate swagger: %s", err) resp.WriteHeader(401) diff --git a/backend/go-app/files.go b/backend/go-app/files.go deleted file mode 100644 index 419d834d..00000000 --- a/backend/go-app/files.go +++ /dev/null @@ -1,882 +0,0 @@ -package main - -/* - Handles files within Workflows.of Shuffle -*/ - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "os" - "strconv" - "strings" - "time" - - "cloud.google.com/go/datastore" - "github.com/satori/go.uuid" -) - -type File struct { - Id string `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - CreatedAt int64 `json:"created_at" datastore:"created_at"` - UpdatedAt int64 `json:"updated_at" datastore:"updated_at"` - MetaAccessAt int64 `json:"meta_access_at" datastore:"meta_access_at"` - DownloadAt int64 `json:"last_downloaded" datastore:"last_downloaded"` - Description string `json:"description" datastore:"description"` - ExpiresAt string `json:"expires_at" datastore:"expires_at"` - Status string `json:"status" datastore:"status"` - Filename string `json:"filename" datastore:"filename"` - URL string `json:"url" datastore:"org"` - OrgId string `json:"org_id" datastore:"org_id"` - WorkflowId string `json:"workflow_id" datastore:"workflow_id"` - Workflows []string `json:"workflows" datastore:"workflows"` - DownloadPath string `json:"download_path" datastore:"download_path"` - Md5sum string `json:"md5_sum" datastore:"md5_sum"` - Sha256sum string `json:"sha256_sum" datastore:"sha256_sum"` - FileSize int64 `json:"filesize" datastore:"filesize"` - Duplicate bool `json:"duplicate" datastore:"duplicate"` - Subflows []string `json:"subflows" datastore:"subflows"` -} - -var basepath = os.Getenv("SHUFFLE_FILE_LOCATION") - -func fileAuthentication(request *http.Request) (string, error) { - executionId, ok := request.URL.Query()["execution_id"] - if ok && len(executionId) > 0 { - ctx := context.Background() - workflowExecution, err := getWorkflowExecution(ctx, executionId[0]) - if err != nil { - log.Printf("[ERROR] Couldn't find execution ID %s", executionId[0]) - return "", err - } - - apikey := request.Header.Get("Authorization") - if !strings.HasPrefix(apikey, "Bearer ") { - log.Printf("[ERROR} Apikey doesn't start with bearer (2)") - return "", errors.New("No auth key found") - } - - apikeyCheck := strings.Split(apikey, " ") - if len(apikeyCheck) != 2 { - log.Printf("[ERROR] Invalid format for apikey (2)") - return "", errors.New("No space in authkey") - } - - // This is annoying af and is done because of maxlength lol - newApikey := apikeyCheck[1] - if newApikey != workflowExecution.Authorization { - //log.Printf("[ERROR] Bad apikey for execution %s. %s vs %s", executionId[0], apikey, workflowExecution.Authorization) - log.Printf("[ERROR] Bad apikey for execution %s.", executionId[0]) - //%s vs %s", executionId[0], apikey, workflowExecution.Authorization) - return "", errors.New("Bad authorization key") - } - - log.Printf("[INFO] Authorization is correct for execution %s!", executionId[0]) - //%s vs %s. Setting Org", executionId, apikey, workflowExecution.Authorization) - if len(workflowExecution.ExecutionOrg) > 0 { - return workflowExecution.ExecutionOrg, nil - } else if len(workflowExecution.Workflow.ExecutingOrg.Id) > 0 { - return workflowExecution.ExecutionOrg, nil - } else { - log.Printf("[ERROR] Couldn't find org for workflow execution, but auth was correct.") - } - } - - return "", errors.New("No execution id specified") -} - -// https://golangcode.com/check-if-a-file-exists/ -func fileExists(filename string) bool { - info, err := os.Stat(filename) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} - -func handleGetFiles(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - // 1. Check user directly - // 2. Check workflow execution authorization - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("[INFO] INITIAL Api authentication failed in file LIST: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if user.Role != "admin" { - log.Printf("[AUTH] User isn't admin") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Need to be admin"}`))) - return - } - - ctx := context.Background() - files, err := getAllFiles(ctx, user.ActiveOrg.Id) - if err != nil { - log.Printf("[ERROR] Failed to get files: %s", err) - resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error getting files."}`))) - return - } - - log.Printf("[INFO] Got %d files for org %s", len(files), user.ActiveOrg.Id) - newBody, err := json.Marshal(files) - if err != nil { - log.Printf("[ERROR] Failed marshaling files: %s", err) - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Failed to marshal files"}`)) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(newBody)) -} - -func handleGetFileMeta(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - var fileId string - location := strings.Split(request.URL.String(), "/") - if location[1] == "api" { - if len(location) <= 4 { - log.Printf("[INFO] Path too short: %d", len(location)) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[4] - } - - if strings.Contains(fileId, "?") { - fileId = strings.Split(fileId, "?")[0] - } - - if len(fileId) != 36 { - log.Printf("Bad format for fileId %s", fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Badly formatted fileId"}`)) - return - } - - log.Printf("\n\n[INFO] User is trying to GET File Meta for %s\n\n", fileId) - - // 1. Check user directly - // 2. Check workflow execution authorization - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("[INFO] INITIAL Api authentication failed in file deletion: %s", err) - - orgId, err := fileAuthentication(request) - if err != nil { - log.Printf("[ERROR] Bad file authentication in get: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user.ActiveOrg.Id = orgId - user.Username = "Execution File API" - } - - // 1. Verify if the user has access to the file: org_id and workflow - log.Printf("[INFO] Should GET FILE META for %s if user has access", fileId) - ctx := context.Background() - file, err := getFile(ctx, fileId) - if err != nil { - log.Printf("[INFO] File %s not found: %s", fileId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - found := false - if file.OrgId == user.ActiveOrg.Id { - found = true - } else { - for _, item := range user.Orgs { - if item == file.OrgId { - found = true - break - } - } - } - - if !found { - log.Printf("[INFO] User %s doesn't have access to %s", user.Username, fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - newBody, err := json.Marshal(file) - if err != nil { - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Failed to marshal filedata"}`)) - return - } - - log.Printf("[INFO] Successfully got file meta for %s", fileId) - resp.WriteHeader(200) - resp.Write([]byte(newBody)) -} - -func handleDeleteFile(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - var fileId string - location := strings.Split(request.URL.String(), "/") - if location[1] == "api" { - if len(location) <= 4 { - log.Printf("[INFO] Path too short: %d", len(location)) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[4] - } - - if strings.Contains(fileId, "?") { - fileId = strings.Split(fileId, "?")[0] - } - - if len(fileId) != 36 { - log.Printf("Bad format for fileId %s", fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Badly formatted fileId"}`)) - return - } - - log.Printf("\n\n[INFO] User is trying to delete file %s\n\n", fileId) - - // 1. Check user directly - // 2. Check workflow execution authorization - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("[INFO] INITIAL Api authentication failed in file deletion: %s", err) - - orgId, err := fileAuthentication(request) - if err != nil { - log.Printf("[ERROR] Bad file authentication in get: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user.ActiveOrg.Id = orgId - user.Username = "Execution File API" - } - - // 1. Verify if the user has access to the file: org_id and workflow - log.Printf("[INFO] Should DELETE file %s if user has access", fileId) - ctx := context.Background() - file, err := getFile(ctx, fileId) - if err != nil { - log.Printf("[INFO] File %s not found: %s", fileId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - found := false - if file.OrgId == user.ActiveOrg.Id { - found = true - } else { - for _, item := range user.Orgs { - if item == file.OrgId { - found = true - break - } - } - } - - if !found { - log.Printf("[INFO] User %s doesn't have access to %s", user.Username, fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if file.Status == "deleted" { - log.Printf("[INFO] File with ID %s is already deleted.", fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if fileExists(file.DownloadPath) { - err = os.Remove(file.DownloadPath) - if err != nil { - log.Printf("[ERROR] Failed deleting file locally: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed deleting filein path %s"}`, file.DownloadPath))) - return - } - - log.Printf("[INFO] Deleted file %s locally. Next is database.", file.DownloadPath) - } else { - log.Printf("[ERROR] File doesn't exist. Can't delete. Should maybe delete file anyway?") - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "File in location %s doesn't exist"}`, file.DownloadPath))) - return - } - - file.Status = "deleted" - err = setFile(ctx, *file) - if err != nil { - log.Printf("[ERROR] Failed setting file to deleted") - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Failed setting file to deleted"}`)) - return - } - - /* - //Actually delete it? - err = DeleteKey(ctx, "files", fileId) - if err != nil { - log.Printf("Failed deleting file with ID %s: %s", fileId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - */ - - log.Printf("[INFO] Successfully deleted file %s", fileId) - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func handleGetFileContent(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - var fileId string - location := strings.Split(request.URL.String(), "/") - if location[1] == "api" { - if len(location) <= 4 { - log.Printf("Path too short: %d", len(location)) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[4] - } - - if len(fileId) != 36 { - log.Printf("Bad format for fileId %s", fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Badly formatted fileId"}`)) - return - } - - log.Printf("\n\n[INFO] User is trying to download file %s\n\n", fileId) - - // 1. Check user directly - // 2. Check workflow execution authorization - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("INITIAL Api authentication failed in file download: %s", err) - - orgId, err := fileAuthentication(request) - if err != nil { - log.Printf("Bad file authentication in get: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user.ActiveOrg.Id = orgId - user.Username = "Execution File API" - /* - } else { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - */ - } - - // 1. Verify if the user has access to the file: org_id and workflow - log.Printf("[INFO] Should get file %s", fileId) - ctx := context.Background() - file, err := getFile(ctx, fileId) - if err != nil { - log.Printf("[ERROR] File %s not found: %s", fileId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - found := false - if file.OrgId == user.ActiveOrg.Id { - found = true - } else { - for _, item := range user.Orgs { - if item == file.OrgId { - found = true - break - } - } - } - - if !found { - log.Printf("User %s doesn't have access to %s", user.Username, fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if file.Status != "active" { - log.Printf("[ERROR] File status isn't active, but %s. Can't continue.", file.Status) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "The file isn't ready to be downloaded yet. Status required: active"}`)) - return - } - - // Fixme: More auth: org and workflow! - downloadPath := file.DownloadPath - log.Printf("[INFO] Downloadpath: %s", downloadPath) - Openfile, err := os.Open(downloadPath) - defer Openfile.Close() //Close after function return - if err != nil { - file.Status = "deleted" - err = setFile(ctx, *file) - if err != nil { - log.Printf("Failed setting file to uploading") - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Failed setting file to uploading"}`)) - return - } - - //File not found, send 404 - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "File doesn't exist locally"}`)) - return - } - - //File is found, create and send the correct headers - //Get the Content-Type of the file - //Create a buffer to store the header of the file in - FileHeader := make([]byte, 512) - //Copy the headers into the FileHeader buffer - Openfile.Read(FileHeader) - //Get content type of file - FileContentType := http.DetectContentType(FileHeader) - - //Get the file size - FileStat, _ := Openfile.Stat() //Get info from file - FileSize := strconv.FormatInt(FileStat.Size(), 10) //Get file size as a string - - //Send the headers - resp.Header().Set("Content-Disposition", "attachment; filename="+fileId) - resp.Header().Set("Content-Type", FileContentType) - resp.Header().Set("Content-Length", FileSize) - - //Send the file - //We read 512 bytes from the file already, so we reset the offset back to 0 - Openfile.Seek(0, 0) - io.Copy(resp, Openfile) //'Copy' the file to the client - return - - //log.Printf("Should download file %s", downloadPath) -} -func handleUploadFile(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - var fileId string - location := strings.Split(request.URL.String(), "/") - if location[1] == "api" { - if len(location) <= 4 { - log.Printf("Path too short: %d", len(location)) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[4] - } - - if len(fileId) != 36 { - log.Printf("Bad format for fileId %s", fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Badly formatted fileId"}`)) - return - } - - // 1. Check user directly - // 2. Check workflow execution authorization - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("INITIAL Api authentication failed in file upload: %s", err) - - orgId, err := fileAuthentication(request) - if err != nil { - log.Printf("Bad file authentication in create file: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user.ActiveOrg.Id = orgId - user.Username = "Execution File API" - } - - log.Printf("[INFO] Should UPLOAD file %s if user has access", fileId) - ctx := context.Background() - file, err := getFile(ctx, fileId) - if err != nil { - log.Printf("File %s not found: %s", fileId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - found := false - if file.OrgId == user.ActiveOrg.Id { - found = true - } else { - for _, item := range user.Orgs { - if item == file.OrgId { - found = true - break - } - } - } - - if !found { - log.Printf("User %s doesn't have access to %s", user.Username, fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - log.Printf("[INFO] STATUS: %s", file.Status) - if file.Status != "created" { - log.Printf("File status isn't created. Can't upload.") - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "This file already has data."}`)) - return - } - - request.ParseMultipartForm(32 << 20) - parsedFile, _, err := request.FormFile("shuffle_file") - if err != nil { - log.Printf("[ERROR] Couldn't upload file: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed uploading file"}`)) - return - } - defer parsedFile.Close() - - file.Status = "uploading" - err = setFile(ctx, *file) - if err != nil { - log.Printf("Failed setting file to uploading") - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Failed setting file to uploading"}`)) - return - } - - // Can be used for validation files for change - var buf bytes.Buffer - io.Copy(&buf, parsedFile) - contents := buf.Bytes() - file.FileSize = int64(len(contents)) - md5 := md5sum(contents) - buf.Reset() - - sha256Sum := sha256.Sum256(contents) - //parsedFile.Reset() - - f, err := os.OpenFile(file.DownloadPath, os.O_WRONLY|os.O_CREATE, os.ModePerm) - if err != nil { - // Rolling back file - file.Status = "created" - setFile(ctx, *file) - - log.Printf("[ERROR] Failed uploading and creating file: %s", err) - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false}`)) - return - } - - defer f.Close() - parsedFile.Seek(0, io.SeekStart) - io.Copy(f, parsedFile) - - // FIXME: Set this one to 200 anyway? Can't download file then tho.. - file.Status = "active" - file.Md5sum = md5 - file.Sha256sum = fmt.Sprintf("%x", sha256Sum) - log.Printf("[INFO] MD5 for file %s (%s) is %s and SHA256 is %s", file.Filename, file.Id, file.Md5sum, file.Sha256sum) - - err = setFile(ctx, *file) - if err != nil { - log.Printf("[ERROR] Failed setting file back to active") - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Failed setting file to active"}`)) - return - } - - log.Printf("[INFO] Successfully uploaded file ID %s", file.Id) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) -} - -func handleCreateFile(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - // 1. Check user directly - // 2. Check workflow execution authorization - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("[INFO] INITIAL Api authentication failed in file creation: %s", err) - - orgId, err := fileAuthentication(request) - if err != nil { - log.Printf("[ERROR] Bad file authentication in create file: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user.ActiveOrg.Id = orgId - user.Username = "Execution File API" - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Println("Failed reading body") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed to read data"}`))) - return - } - - type FileStructure struct { - Filename string `json:"filename"` - OrgId string `json:"org_id"` - WorkflowId string `json:"workflow_id"` - } - - var curfile FileStructure - err = json.Unmarshal(body, &curfile) - if err != nil { - log.Printf("[ERROR] Failed unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed to unmarshal data"}`))) - return - } - - // Loads of validation below - if len(curfile.Filename) == 0 || len(curfile.OrgId) == 0 || len(curfile.WorkflowId) == 0 { - log.Printf("[ERROR] Missing field during fileupload. Required: filename, org_id, workflow_id") - log.Printf("INPUT: %s", string(body)) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Missing field. Required: filename, org_id, workflow_id"}`))) - return - } - - ctx := context.Background() - if user.ActiveOrg.Id != curfile.OrgId { - log.Printf("[ERROR] User can't access org %s", curfile.OrgId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Error with organization"}`)) - return - } - - var workflow *Workflow - if curfile.WorkflowId == "global" { - // PS: Not a security issue. - // Files are global anyway, but the workflow_id is used to identify origin - log.Printf("[INFO] Uploading filename %s for org %s as global file.", curfile.Filename, curfile.OrgId) - } else { - // Try to get the org and workflow in case they don't exist - workflow, err = getWorkflow(ctx, curfile.WorkflowId) - if err != nil { - log.Printf("[ERROR] Workflow %s doesn't exist.", curfile.WorkflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Error with workflow id or org id"}`)) - return - } - - _, err = getOrg(ctx, curfile.OrgId) - if err != nil { - log.Printf("[ERROR] Org %s doesn't exist.", curfile.OrgId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Error with workflow id or org id"}`)) - return - } - - if workflow.ExecutingOrg.Id != curfile.OrgId { - found := false - for _, curorg := range workflow.Org { - if curorg.Id == curfile.OrgId { - found = true - break - } - } - - if !found { - log.Printf("[ERROR] Org %s doesn't have access to %s.", curfile.OrgId, curfile.WorkflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Error with workflow id or org id"}`)) - return - } - } - } - - if strings.Contains(curfile.Filename, "/") || strings.Contains(curfile.Filename, `"`) || strings.Contains(curfile.Filename, "..") || strings.Contains(curfile.Filename, "~") { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Invalid characters in filename"}`)) - return - } - - // 1. Create the file object. - if len(basepath) == 0 { - basepath = "shuffle-files" - } - folderPath := fmt.Sprintf("%s/%s/%s", basepath, curfile.OrgId, curfile.WorkflowId) - - // Try to make the full file location - err = os.MkdirAll(folderPath, os.ModePerm) - if err != nil { - log.Printf("[ERROR] Writing issue for file location creation: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed creating upload location"}`)) - return - } - - filename := curfile.Filename - fileId := uuid.NewV4().String() - downloadPath := fmt.Sprintf("%s/%s", folderPath, fileId) - - duplicateWorkflows := []string{} - if curfile.WorkflowId != "global" { - for _, trigger := range workflow.Triggers { - if trigger.AppName == "Shuffle Workflow" && trigger.TriggerType == "SUBFLOW" { - for _, parameter := range trigger.Parameters { - if parameter.Name == "workflow" && len(parameter.Value) > 0 { - - found := false - for _, workflow := range duplicateWorkflows { - if workflow == parameter.Value { - found = true - break - } - } - - if !found { - duplicateWorkflows = append(duplicateWorkflows, parameter.Value) - } - - break - } - } - } - } - } - - timeNow := time.Now().Unix() - newFile := File{ - Id: fileId, - CreatedAt: timeNow, - UpdatedAt: timeNow, - Description: "", - Status: "created", - Filename: filename, - OrgId: curfile.OrgId, - WorkflowId: curfile.WorkflowId, - DownloadPath: downloadPath, - Subflows: duplicateWorkflows, - } - - err = setFile(ctx, newFile) - if err != nil { - log.Printf("[ERROR] Failed setting file: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed setting file reference"}`)) - return - } else { - log.Printf("[INFO] Created file %s", newFile.DownloadPath) - } - - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "id": "%s"}`, fileId))) - -} - -func getFile(ctx context.Context, id string) (*File, error) { - key := datastore.NameKey("Files", id, nil) - curFile := &File{} - if err := dbclient.Get(ctx, key, curFile); err != nil { - return &File{}, err - } - - return curFile, nil -} - -func setFile(ctx context.Context, file File) error { - // clear session_token and API_token for user - timeNow := time.Now().Unix() - file.UpdatedAt = timeNow - - k := datastore.NameKey("Files", file.Id, nil) - if _, err := dbclient.Put(ctx, k, &file); err != nil { - log.Println(err) - return err - } - - return nil -} - -func getAllFiles(ctx context.Context, orgId string) ([]File, error) { - var files []File - q := datastore.NewQuery("Files").Filter("org_id =", orgId).Order("-updated_at").Limit(100) - - _, err := dbclient.GetAll(ctx, q, &files) - if err != nil { - if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { - q = q.Limit(50) - _, err := dbclient.GetAll(ctx, q, &files) - if err != nil { - return []File{}, err - } - } else { - return []File{}, err - } - } - - return files, nil -} diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 0bf0ccce..e403c8bd 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,11 +2,15 @@ module shuffle go 1.13 +replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared + +replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi + require ( - cloud.google.com/go v0.57.0 - cloud.google.com/go/datastore v1.1.0 + cloud.google.com/go v0.75.0 + cloud.google.com/go/datastore v1.4.0 cloud.google.com/go/pubsub v1.3.1 - cloud.google.com/go/storage v1.7.0 + cloud.google.com/go/storage v1.12.0 github.com/Microsoft/go-winio v0.4.14 // indirect github.com/basgys/goxml2json v1.1.0 github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 @@ -14,24 +18,26 @@ require ( github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect - github.com/getkin/kin-openapi v0.8.0 + github.com/frikky/shuffle-shared v0.0.12 // indirect + github.com/getkin/kin-openapi v0.52.0 // indirect + //github.com/getkin/kin-openapi v0.8.0 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0 github.com/google/go-github/v28 v28.1.1 github.com/gorilla/handlers v1.4.2 // indirect - github.com/gorilla/mux v1.7.4 + github.com/gorilla/mux v1.8.0 github.com/h2non/filetype v1.0.12 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible github.com/satori/go.uuid v1.2.0 - golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - google.golang.org/api v0.23.0 - google.golang.org/appengine v1.6.6 - google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31 - google.golang.org/grpc v1.29.1 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423 + google.golang.org/api v0.36.0 + google.golang.org/appengine v1.6.7 + google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595 + google.golang.org/grpc v1.34.1 gopkg.in/src-d/go-git.v4 v4.13.1 - gopkg.in/yaml.v2 v2.2.8 - gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 + gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 2d63be91..de114e19 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -12,14 +12,24 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.6.0/go.mod h1:hyFDG0qSGdHNz8Q6nDN8rYIkld0q/+5uBZaelxiDLfE= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.4.0 h1:CFDJm15RpYXeEblQ0TMDUrYtqmBmbAWTy536nA8JIc8= +cloud.google.com/go/datastore v1.4.0/go.mod h1:d18825/a9bICdAIJy2EkHs9joU4RlIZ1t6l8WDdbdY0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -30,6 +40,10 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.7.0 h1:DzdLPI8Em+DEk7IzA2a10ivq3mxIEASC9GeNJ6FFt5Q= cloud.google.com/go/storage v1.7.0/go.mod h1:jGMIBwF+L/tL6WN/W5InNgYYu4HP0DvGB6rQ1mufWfs= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.12.0 h1:4y3gHptW1EHVtcPAVE0eBBlFuGqEejTTG3KdIE0lUX4= +cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -49,6 +63,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -66,10 +81,17 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/frikky/kin-openapi v0.38.0 h1:V7ttwIJS8Vks4KL+mZVj1ZSqhIcQtgaG8akeqXEQgsE= +github.com/frikky/kin-openapi v0.38.0/go.mod h1:Fr28TtCHL4K0kIqtqui8HWxN1LG5uAh3z/tDfFyiA1s= +github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu2YUSjhw= +github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww= github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE= github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw= +github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M= +github.com/getkin/kin-openapi v0.52.0/go.mod h1:fRpo2Nw4Czgy0QnrIesRrEXs5+15N1F9mGZLP/aIomE= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -85,6 +107,10 @@ github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmC github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -96,6 +122,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -108,6 +135,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -115,19 +146,32 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -135,11 +179,14 @@ github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YAR github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/h2non/filetype v1.0.12 h1:yHCsIe0y2cvbDARtJhGBTD2ecvqMSTvlIcph9En/Zao= github.com/h2non/filetype v1.0.12/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -155,6 +202,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -183,15 +233,21 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -201,6 +257,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88= golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -224,6 +282,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -232,6 +291,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -242,6 +304,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -252,12 +315,28 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423 h1:/hEknzWkMPCjTo7StMHRrBRa8YBbXuBWfck8680k3RE= +golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -266,6 +345,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -291,11 +373,25 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e h1:hq86ru83GdWTlfQFZGO4nZJTU4Bs2wfHl8oFHRaXsfc= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -336,10 +432,25 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200409170454-77362c5149f0/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d h1:lzLdP95xJmMpwQ6LUHwrc5V7js93hTiY7gkznu0BgmY= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -355,6 +466,15 @@ google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.23.0 h1:YlvGEOq2NA2my8cZ/9V8BcEO9okD48FlJcdqN0xJL3s= google.golang.org/api v0.23.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo= +google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0 h1:l2Nfbl2GPXdWorv+dT2XfinX2jOOw4zv1VhLstx+6rE= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -362,6 +482,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -387,6 +509,22 @@ google.golang.org/genproto v0.0.0-20200409111301-baae70f3302d/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31 h1:Bz1qTn2YRWV+9OKJtxHJiQKCiXIdf+kwuKXdt9cBxyU= google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595 h1:x7nk+/4+SvuTDI4wnzQUlhvi+DTpyfncXBo3QWTFs7U= +google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -399,12 +537,26 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.34.1 h1:ugq+9++ZQPFzM2pKUMCIK8gj9M0pFyuUWO9Q8kwEDQw= +google.golang.org/grpc v1.34.1/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -421,8 +573,14 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0= gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -430,6 +588,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index deeb5bc8..11df5536 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1,6 +1,8 @@ package main import ( + "github.com/frikky/shuffle-shared" + "bufio" "bytes" @@ -29,12 +31,11 @@ import ( "cloud.google.com/go/datastore" "cloud.google.com/go/pubsub" "cloud.google.com/go/storage" - "google.golang.org/api/option" "google.golang.org/appengine/mail" - "github.com/getkin/kin-openapi/openapi2" - "github.com/getkin/kin-openapi/openapi2conv" - "github.com/getkin/kin-openapi/openapi3" + "github.com/frikky/kin-openapi/openapi2" + "github.com/frikky/kin-openapi/openapi2conv" + "github.com/frikky/kin-openapi/openapi3" /* "github.com/frikky/kin-openapi/openapi2" "github.com/frikky/kin-openapi/openapi2conv" @@ -63,6 +64,7 @@ import ( // Web "github.com/gorilla/mux" "github.com/patrickmn/go-cache" + "google.golang.org/api/option" "google.golang.org/grpc" http2 "gopkg.in/src-d/go-git.v4/plumbing/transport/http" ) @@ -150,11 +152,11 @@ type UserLimits struct { } type retStruct struct { - Success bool `json:"success"` - SyncFeatures SyncFeatures `json:"sync_features"` - SessionKey string `json:"session_key"` - IntervalSeconds int64 `json:"interval_seconds"` - Reason string `json:"reason"` + Success bool `json:"success"` + SyncFeatures shuffle.SyncFeatures `json:"sync_features"` + SessionKey string `json:"session_key"` + IntervalSeconds int64 `json:"interval_seconds"` + Reason string `json:"reason"` } // Saves some data, not sure what to have here lol @@ -172,37 +174,15 @@ type UserAuthField struct { } // Not environment, but execution environment -type Environment struct { - Name string `datastore:"name"` - Type string `datastore:"type"` - Registered bool `datastore:"registered"` - Default bool `datastore:"default" json:"default"` - Archived bool `datastore:"archived" json:"archived"` - Id string `datastore:"id" json:"id"` - OrgId string `datastore:"org_id" json:"org_id"` -} - -type User struct { - Username string `datastore:"Username" json:"username"` - Password string `datastore:"password,noindex" password:"password,omitempty"` - Session string `datastore:"session,noindex" json:"session"` - Verified bool `datastore:"verified,noindex" json:"verified"` - PrivateApps []WorkflowApp `datastore:"privateapps" json:"privateapps":` - Role string `datastore:"role" json:"role"` - Roles []string `datastore:"roles" json:"roles"` - VerificationToken string `datastore:"verification_token" json:"verification_token"` - ApiKey string `datastore:"apikey" json:"apikey"` - ResetReference string `datastore:"reset_reference" json:"reset_reference"` - Executions ExecutionInfo `datastore:"executions" json:"executions"` - Limits UserLimits `datastore:"limits" json:"limits"` - Authentication []UserAuth `datastore:"authentication,noindex" json:"authentication"` - ResetTimeout int64 `datastore:"reset_timeout,noindex" json:"reset_timeout"` - Id string `datastore:"id" json:"id"` - Orgs []string `datastore:"orgs" json:"orgs"` - CreationTime int64 `datastore:"creation_time" json:"creation_time"` - ActiveOrg Org `json:"active_org" datastore:"active_org"` - Active bool `datastore:"active" json:"active"` -} +//type Environment struct { +// Name string `datastore:"name"` +// Type string `datastore:"type"` +// Registered bool `datastore:"registered"` +// Default bool `datastore:"default" json:"default"` +// Archived bool `datastore:"archived" json:"archived"` +// Id string `datastore:"id" json:"id"` +// OrgId string `datastore:"org_id" json:"org_id"` +//} // timeout maybe? idk type session struct { @@ -611,83 +591,6 @@ func checkFileExistsLocal(basepath string, filepath string) bool { return true } -func handleApiAuthentication(resp http.ResponseWriter, request *http.Request) (User, error) { - apikey := request.Header.Get("Authorization") - if len(apikey) > 0 { - if !strings.HasPrefix(apikey, "Bearer ") { - log.Printf("[WARNING] Apikey doesn't start with bearer") - return User{}, errors.New("No bearer token for authorization header") - } - - apikeyCheck := strings.Split(apikey, " ") - if len(apikeyCheck) != 2 { - log.Printf("[WARNING] Invalid format for apikey.") - return User{}, errors.New("Invalid format for apikey") - } - - // This is annoying af and is done because of maxlength lol - newApikey := apikeyCheck[1] - if len(newApikey) > 249 { - newApikey = newApikey[0:248] - } - - ctx := context.Background() - - // Make specific check for just service user? - // Get the user based on APIkey here - Userdata, err := getApikey(ctx, apikeyCheck[1]) - if err != nil { - log.Printf("Apikey %s doesn't exist: %s", apikey, err) - return User{}, err - } - - if len(Userdata.Username) > 0 { - return Userdata, nil - } else { - return Userdata, errors.New(fmt.Sprintf("[WARNING] User is invalid - no username found")) - } - } - - // One time API keys - authorizationArr, ok := request.URL.Query()["authorization"] - ctx := context.Background() - if ok { - authorization := "" - if len(authorizationArr) > 0 { - authorization = authorizationArr[0] - } - _ = authorization - } - - c, err := request.Cookie("session_token") - if err == nil { - sessionToken := c.Value - session, err := getSession(ctx, sessionToken) - if err != nil { - log.Printf("[WARNING] Session %s doesn't exist (session auth): %s", sessionToken, err) - return User{}, err - } - - // Get session first - // Should basically never happen - Userdata, err := getUser(ctx, session.Id) - if err != nil { - log.Printf("[INFO] Username %s doesn't exist (authcheck): %s", session.Username, err) - return User{}, err - } - - if Userdata.Session != sessionToken { - return User{}, errors.New("Wrong session token") - } - - // Means session exists, but - return *Userdata, nil - } - - // Key = apikey - return User{}, errors.New("Missing authentication") -} - func handleGetallSchedules(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -800,7 +703,7 @@ func deleteUser(resp http.ResponseWriter, request *http.Request) { return } - userInfo, userErr := handleApiAuthentication(resp, request) + userInfo, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in edit workflow: %s", userErr) resp.WriteHeader(401) @@ -828,7 +731,7 @@ func deleteUser(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - foundUser, err := getUser(ctx, userId) + foundUser, err := shuffle.GetUser(ctx, userId) if err != nil { log.Printf("Can't find user %s (delete user): %s", userId, err) resp.WriteHeader(401) @@ -864,7 +767,7 @@ func deleteUser(resp http.ResponseWriter, request *http.Request) { foundUser.Active = true } - err = setUser(ctx, foundUser) + err = shuffle.SetUser(ctx, foundUser) if err != nil { log.Printf("Failed swapping active for user %s (%s)", foundUser.Username, foundUser.Id) resp.WriteHeader(401) @@ -920,7 +823,7 @@ func handleRegisterVerification(resp http.ResponseWriter, request *http.Request) // With user, do a search for workflows with user or user's org attached // Only giving 200 to not give any suspicion whether they're onto an actual user or not q := datastore.NewQuery("Users").Filter("verification_token =", reference) - var users []User + var users []shuffle.User _, err := dbclient.GetAll(ctx, q, &users) if err != nil { log.Printf("Failed getting users for verification token: %s", err) @@ -941,7 +844,7 @@ func handleRegisterVerification(resp http.ResponseWriter, request *http.Request) // FIXME: Not for cloud! Userdata.Verified = true - err = setUser(ctx, &Userdata) + err = shuffle.SetUser(ctx, &Userdata) if err != nil { log.Printf("Failed adding verification for user %s: %s", Userdata.Username, err) resp.WriteHeader(401) @@ -962,7 +865,7 @@ func handleSetEnvironments(resp http.ResponseWriter, request *http.Request) { // FIXME: Overhaul the top part. // Only admin can change environments, but if there are no users, anyone can make (first) - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Can't handle set env auth"}`)) @@ -976,7 +879,7 @@ func handleSetEnvironments(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - var environments []Environment + var environments []shuffle.Environment q := datastore.NewQuery("Environments").Filter("org_id =", user.ActiveOrg.Id) _, err = dbclient.GetAll(ctx, q, &environments) if err != nil { @@ -993,7 +896,7 @@ func handleSetEnvironments(resp http.ResponseWriter, request *http.Request) { return } - var newEnvironments []Environment + var newEnvironments []shuffle.Environment err = json.Unmarshal(body, &newEnvironments) if err != nil { log.Printf("Failed unmarshaling: %s", err) @@ -1053,7 +956,7 @@ func handleSetEnvironments(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": true}`)) } -func createNewUser(username, password, role, apikey string, org Org) error { +func createNewUser(username, password, role, apikey string, org shuffle.Org) error { // Returns false if there is an issue // Use this for register err := checkPasswordStrength(password) @@ -1070,7 +973,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { ctx := context.Background() q := datastore.NewQuery("Users").Filter("Username =", username) - var users []User + var users []shuffle.User _, err = dbclient.GetAll(ctx, q, &users) if err != nil { log.Printf("Failed getting user for registration: %s", err) @@ -1087,7 +990,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { return err } - newUser := new(User) + newUser := new(shuffle.User) newUser.Username = username newUser.Password = string(hashedPassword) newUser.Verified = false @@ -1136,13 +1039,13 @@ func createNewUser(username, password, role, apikey string, org Org) error { newUser.Id = ID.String() newUser.VerificationToken = verifyToken.String() - err = setUser(ctx, newUser) + err = shuffle.SetUser(ctx, newUser) if err != nil { log.Printf("Error adding User %s: %s", username, err) return err } - neworg, err := getOrg(ctx, org.Id) + neworg, err := shuffle.GetOrg(ctx, org.Id) if err == nil { //neworg.Users = append(neworg.Users, *newUser) err = setOrg(ctx, *neworg, neworg.Id) @@ -1170,7 +1073,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) { // FIXME: Overhaul the top part. // Only admin can CREATE users, but if there are no users, anyone can make (first) count, countErr := getUserCount() - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { if (countErr == nil && count > 0) || countErr != nil { resp.WriteHeader(401) @@ -1206,7 +1109,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) { if user.ActiveOrg.Id == "" { log.Printf("There's no active org for the user. Checking if there's a single one to assing it to.") - var orgs []Org + var orgs []shuffle.Org q := datastore.NewQuery("Organizations") _, err = dbclient.GetAll(ctx, q, &orgs) if err == nil && len(orgs) == 1 { @@ -1255,7 +1158,7 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { Expires: time.Unix(0, 0), }) - userInfo, err := handleApiAuthentication(resp, request) + userInfo, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in handleLogout: %s", err) resp.WriteHeader(200) @@ -1264,7 +1167,7 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - session, err := getSession(ctx, userInfo.Session) + session, err := shuffle.GetSession(ctx, userInfo.Session) if err != nil { log.Printf("Session %#v doesn't exist: %s", session, err) resp.WriteHeader(401) @@ -1295,7 +1198,7 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { // Get session first // Should basically never happen - //_, err = getUser(ctx, session.Id) + //_, err = shuffle.GetUser(ctx, session.Id) //if err != nil { // log.Printf("Username %s doesn't exist (logout): %s", session.Username, err) // resp.WriteHeader(401) @@ -1309,7 +1212,7 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { // FIXME // Session might delete someone elses here? // No need to think about before possible scale..? - err = SetSession(ctx, userInfo, "") + err = shuffle.SetSession(ctx, userInfo, "") if err != nil { log.Printf("Error removing session for: %s", err) resp.WriteHeader(401) @@ -1326,7 +1229,7 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { } userInfo.Session = "" - err = setUser(ctx, &userInfo) + err = shuffle.SetUser(ctx, &userInfo) if err != nil { log.Printf("Failed updating user: %s", err) resp.WriteHeader(401) @@ -1340,35 +1243,13 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": false, "reason": "Successfully logged out"}`)) } -func generateApikey(ctx context.Context, userInfo User) (User, error) { - // Generate UUID - // Set uuid to apikey in backend (update) - apikey := uuid.NewV4() - userInfo.ApiKey = apikey.String() - - err := SetApikey(ctx, userInfo) - if err != nil { - log.Printf("Failed updating apikey: %s", err) - return userInfo, err - } - - // Updating user - err = setUser(ctx, &userInfo) - if err != nil { - log.Printf("Failed updating user: %s", err) - return userInfo, err - } - - return userInfo, nil -} - func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { return } - userInfo, err := handleApiAuthentication(resp, request) + userInfo, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in apigen: %s", err) resp.WriteHeader(401) @@ -1409,7 +1290,7 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { return } - foundUser, err := getUser(ctx, t.UserId) + foundUser, err := shuffle.GetUser(ctx, t.UserId) if err != nil { log.Printf("Can't find user %s (update user): %s", t.UserId, err) resp.WriteHeader(401) @@ -1452,7 +1333,7 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { if len(t.Username) > 0 { q := datastore.NewQuery("Users").Filter("username =", t.Username) - var users []User + var users []shuffle.User _, err = dbclient.GetAll(ctx, q, &users) if err != nil { resp.WriteHeader(401) @@ -1477,7 +1358,7 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { foundUser.Username = t.Username } - err = setUser(ctx, foundUser) + err = shuffle.SetUser(ctx, foundUser) if err != nil { log.Printf("Error patching user %s: %s", foundUser.Username, err) resp.WriteHeader(401) @@ -1495,7 +1376,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { return } - userInfo, err := handleApiAuthentication(resp, request) + userInfo, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in apigen: %s", err) resp.WriteHeader(401) @@ -1505,7 +1386,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() if request.Method == "GET" { - newUserInfo, err := generateApikey(ctx, userInfo) + newUserInfo, err := shuffle.GenerateApikey(ctx, userInfo) if err != nil { log.Printf("Failed to generate apikey for user %s: %s", userInfo.Username, err) resp.WriteHeader(401) @@ -1544,7 +1425,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { return } - foundUser, err := getUser(ctx, t.UserId) + foundUser, err := shuffle.GetUser(ctx, t.UserId) if err != nil { log.Printf("Can't find user %s (apikey gen): %s", t.UserId, err) resp.WriteHeader(401) @@ -1552,7 +1433,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { return } - newUserInfo, err := generateApikey(ctx, *foundUser) + newUserInfo, err := shuffle.GenerateApikey(ctx, *foundUser) if err != nil { log.Printf("Failed to generate apikey for user %s: %s", foundUser.Username, err) resp.WriteHeader(401) @@ -1576,7 +1457,7 @@ func handleSettings(resp http.ResponseWriter, request *http.Request) { return } - userInfo, err := handleApiAuthentication(resp, request) + userInfo, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in apigen: %s", err) resp.WriteHeader(401) @@ -1594,7 +1475,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { return } - userInfo, err := handleApiAuthentication(resp, request) + userInfo, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in handleInfo: %s", err) resp.WriteHeader(401) @@ -1623,7 +1504,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() q := datastore.NewQuery("Users") - var users []User + var users []shuffle.User _, err = dbclient.GetAll(ctx, q, &users) if err != nil { resp.WriteHeader(401) @@ -1688,14 +1569,14 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { // Updating user info if there's something wrong if (len(userInfo.ActiveOrg.Name) == 0 || len(userInfo.ActiveOrg.Id) == 0) && len(userInfo.Orgs) > 0 { - _, err := getOrg(ctx, userInfo.Orgs[0]) + _, err := shuffle.GetOrg(ctx, userInfo.Orgs[0]) if err != nil { - var orgs []Org + var orgs []shuffle.Org q := datastore.NewQuery("Organizations") _, err = dbclient.GetAll(ctx, q, &orgs) if err == nil { newStringOrgs := []string{} - newOrgs := []Org{} + newOrgs := []shuffle.Org{} for _, org := range orgs { if strings.ToLower(org.Name) == strings.ToLower(userInfo.Orgs[0]) { newOrgs = append(newOrgs, org) @@ -1707,7 +1588,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { userInfo.ActiveOrg = newOrgs[0] userInfo.Orgs = newStringOrgs - err = setUser(ctx, &userInfo) + err = shuffle.SetUser(ctx, &userInfo) if err != nil { log.Printf("Error patching User for activeOrg: %s", err) } else { @@ -1721,10 +1602,10 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { } else { // 1. Check if the org exists by ID // 2. if it does, overwrite user - userInfo.ActiveOrg = Org{ + userInfo.ActiveOrg = shuffle.Org{ Id: userInfo.Orgs[0], } - err = setUser(ctx, &userInfo) + err = shuffle.SetUser(ctx, &userInfo) if err != nil { log.Printf("Error patching User for activeOrg: %s", err) } @@ -1732,10 +1613,10 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { } // FIXME: Remove this dependency by updating users' orgs when org itself is updated - org, err := getOrg(ctx, userInfo.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, userInfo.ActiveOrg.Id) if err == nil { userInfo.ActiveOrg = *org - userInfo.ActiveOrg.Users = []User{} + userInfo.ActiveOrg.Users = []shuffle.User{} } currentOrg, err := json.Marshal(userInfo.ActiveOrg) @@ -1816,7 +1697,7 @@ func handlePasswordReset(resp http.ResponseWriter, request *http.Request) { // With user, do a search for workflows with user or user's org attached // Only giving 200 to not give any suspicion whether they're onto an actual user or not q := datastore.NewQuery("Users").Filter("reset_reference =", t.Reference) - var users []User + var users []shuffle.User _, err = dbclient.GetAll(ctx, q, &users) if err != nil { log.Printf("Failed getting users: %s", err) @@ -1845,7 +1726,7 @@ func handlePasswordReset(resp http.ResponseWriter, request *http.Request) { Userdata.Password = string(hashedPassword) Userdata.ResetTimeout = 0 Userdata.ResetReference = "" - err = setUser(ctx, &Userdata) + err = shuffle.SetUser(ctx, &Userdata) if err != nil { log.Printf("Error adding User %s: %s", Userdata.Username, err) resp.WriteHeader(200) @@ -1884,7 +1765,7 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { return } - userInfo, err := handleApiAuthentication(resp, request) + userInfo, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -1929,11 +1810,11 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - foundUser := User{} + foundUser := shuffle.User{} if !curUserFound { log.Printf("Have to find a different user") q := datastore.NewQuery("Users").Filter("Username =", strings.ToLower(t.Username)) - var users []User + var users []shuffle.User _, err = dbclient.GetAll(ctx, q, &users) if err != nil { log.Printf("Failed getting user %s", t.Username) @@ -1998,7 +1879,7 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { } userInfo.Password = string(hashedPassword) - err = setUser(ctx, &foundUser) + err = shuffle.SetUser(ctx, &foundUser) if err != nil { log.Printf("Error fixing password for user %s: %s", userInfo.Username, err) resp.WriteHeader(401) @@ -2087,7 +1968,7 @@ func handleGetSchedules(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -2130,7 +2011,7 @@ func handleGetEnvironments(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -2139,7 +2020,7 @@ func handleGetEnvironments(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - var environments []Environment + var environments []shuffle.Environment q := datastore.NewQuery("Environments").Filter("org_id =", user.ActiveOrg.Id) _, err = dbclient.GetAll(ctx, q, &environments) if err != nil { @@ -2181,7 +2062,7 @@ func handleGetOrg(resp http.ResponseWriter, request *http.Request) { fileId = location[4] } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -2190,7 +2071,7 @@ func handleGetOrg(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - org, err := getOrg(ctx, fileId) + org, err := shuffle.GetOrg(ctx, fileId) if err != nil { resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Failed getting org users"}`)) @@ -2212,7 +2093,7 @@ func handleGetOrg(resp http.ResponseWriter, request *http.Request) { return } - org.Users = []User{} + org.Users = []shuffle.User{} org.SyncConfig.Apikey = "" newjson, err := json.Marshal(org) if err != nil { @@ -2232,7 +2113,7 @@ func handleGetOrgs(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -2247,7 +2128,7 @@ func handleGetOrgs(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - var orgs []Org + var orgs []shuffle.Org q := datastore.NewQuery("Organizations") _, err = dbclient.GetAll(ctx, q, &orgs) if err != nil { @@ -2287,7 +2168,7 @@ func handleGetUsers(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -2303,14 +2184,14 @@ func handleGetUsers(resp http.ResponseWriter, request *http.Request) { // FIXME: Check by org. ctx := context.Background() - org, err := getOrg(ctx, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) if err != nil { resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Failed getting org users"}`)) return } - newUsers := []User{} + newUsers := []shuffle.User{} for _, item := range org.Users { if len(item.Username) == 0 { continue @@ -2390,7 +2271,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() log.Printf("[INFO] Login Username: %s", data.Username) q := datastore.NewQuery("Users").Filter("Username =", data.Username) - var users []User + var users []shuffle.User _, err = dbclient.GetAll(ctx, q, &users) if err != nil { log.Printf("Failed getting user %s", data.Username) @@ -2438,7 +2319,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { loginData = fmt.Sprintf(`{"success": true, "cookies": [{"key": "session_token", "value": "%s", "expiration": %d}]}`, Userdata.Session, expiration.Unix()) //log.Printf("SESSION LENGTH MORE THAN 0 IN LOGIN: %s", Userdata.Session) - err = SetSession(ctx, Userdata, Userdata.Session) + err = shuffle.SetSession(ctx, Userdata, Userdata.Session) if err != nil { log.Printf("Error adding session to database: %s", err) } @@ -2458,13 +2339,13 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { }) // ADD TO DATABASE - err = SetSession(ctx, Userdata, sessionToken) + err = shuffle.SetSession(ctx, Userdata, sessionToken) if err != nil { log.Printf("Error adding session to database: %s", err) } Userdata.Session = sessionToken - err = setUser(ctx, &Userdata) + err = shuffle.SetUser(ctx, &Userdata) if err != nil { log.Printf("Failed updating user when setting session: %s", err) resp.WriteHeader(500) @@ -2481,46 +2362,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(loginData)) } -func getApikey(ctx context.Context, apikey string) (User, error) { - // Query for the specifci workflowId - q := datastore.NewQuery("Users").Filter("apikey =", apikey) - var users []User - _, err := dbclient.GetAll(ctx, q, &users) - if err != nil { - log.Printf("[ERROR] Error getting users apikey (getapikey): %s", err) - return User{}, err - } - - if len(users) == 0 { - log.Printf("[WARNING] No users found for apikey %s", apikey) - return User{}, err - } - - return users[0], nil -} - -func getSession(ctx context.Context, thissession string) (*session, error) { - key := datastore.NameKey("sessions", thissession, nil) - curUser := &session{} - if err := dbclient.Get(ctx, key, curUser); err != nil { - return &session{}, err - } - - return curUser, nil -} - -// ListBooks returns a list of books, ordered by title. -func getOrg(ctx context.Context, id string) (*Org, error) { - key := datastore.NameKey("Organizations", id, nil) - curOrg := &Org{} - if err := dbclient.Get(ctx, key, curOrg); err != nil { - return &Org{}, err - } - - return curOrg, nil -} - -func setOrg(ctx context.Context, org Org, id string) error { +func setOrg(ctx context.Context, org shuffle.Org, id string) error { // clear session_token and API_token for user timeNow := int64(time.Now().Unix()) if org.Created == 0 { @@ -2543,15 +2385,15 @@ func setOrg(ctx context.Context, org Org, id string) error { } // ListBooks returns a list of books, ordered by title. -func getUser(ctx context.Context, id string) (*User, error) { - key := datastore.NameKey("Users", id, nil) - curUser := &User{} - if err := dbclient.Get(ctx, key, curUser); err != nil { - return &User{}, err - } - - return curUser, nil -} +//func getUser(ctx context.Context, id string) (*User, error) { +// key := datastore.NameKey("Users", id, nil) +// curUser := &User{} +// if err := dbclient.Get(ctx, key, curUser); err != nil { +// return &User{}, err +// } +// +// return curUser, nil +//} // Index = Username func DeleteKeys(ctx context.Context, entity string, value []string) error { @@ -2584,52 +2426,6 @@ func DeleteKey(ctx context.Context, entity string, value string) error { return nil } -// Index = Username -func SetApikey(ctx context.Context, Userdata User) error { - // Non indexed User data - newapiUser := new(Userapi) - newapiUser.ApiKey = Userdata.ApiKey - newapiUser.Username = Userdata.Username - key1 := datastore.NameKey("apikey", newapiUser.ApiKey, nil) - - // New struct, to not add body, author etc - if _, err := dbclient.Put(ctx, key1, newapiUser); err != nil { - log.Printf("Error adding apikey: %s", err) - return err - } - - return nil -} - -// Index = Username -func SetSession(ctx context.Context, Userdata User, value string) error { - // Non indexed User data - Userdata.Session = value - key1 := datastore.NameKey("Users", Userdata.Id, nil) - - // New struct, to not add body, author etc - if _, err := dbclient.Put(ctx, key1, &Userdata); err != nil { - log.Printf("rror adding Usersession: %s", err) - return err - } - - if len(Userdata.Session) > 0 { - // Indexed session data - sessiondata := new(session) - sessiondata.Username = Userdata.Username - sessiondata.Session = Userdata.Session - sessiondata.Id = Userdata.Id - key2 := datastore.NameKey("sessions", sessiondata.Session, nil) - - if _, err := dbclient.Put(ctx, key2, sessiondata); err != nil { - log.Printf("Error adding session: %s", err) - return err - } - } - - return nil -} - func setOpenApiDatastore(ctx context.Context, id string, data ParsedOpenApi) error { k := datastore.NameKey("openapi3", id, nil) if _, err := dbclient.Put(ctx, k, &data); err != nil { @@ -2649,7 +2445,7 @@ func getOpenApiDatastore(ctx context.Context, id string) (ParsedOpenApi, error) return *api, nil } -func setEnvironment(ctx context.Context, data *Environment) error { +func setEnvironment(ctx context.Context, data *shuffle.Environment) error { // clear session_token and API_token for user k := datastore.NameKey("Environments", strings.ToLower(data.Name), nil) @@ -2663,7 +2459,7 @@ func setEnvironment(ctx context.Context, data *Environment) error { return nil } -func fixOrgUser(ctx context.Context, org *Org) *Org { +func fixOrgUser(ctx context.Context, org *shuffle.Org) *shuffle.Org { //found := false //for _, id := range user.Orgs { // if user.ActiveOrg.Id == id { @@ -2682,7 +2478,7 @@ func fixOrgUser(ctx context.Context, org *Org) *Org { // continue // } - // org, err := getOrg(ctx, orgId) + // org, err := shuffle.GetOrg(ctx, orgId) // if err != nil { // log.Printf("Error getting org %s", orgId) // continue @@ -2718,21 +2514,7 @@ func fixOrgUser(ctx context.Context, org *Org) *Org { return org } -// ListBooks returns a list of books, ordered by title. -func setUser(ctx context.Context, data *User) error { - data = fixUserOrg(ctx, data) - - // clear session_token and API_token for user - k := datastore.NameKey("Users", data.Id, nil) - if _, err := dbclient.Put(ctx, k, data); err != nil { - log.Println(err) - return err - } - - return nil -} - -func fixUserOrg(ctx context.Context, user *User) *User { +func fixUserOrg(ctx context.Context, user *shuffle.User) *shuffle.User { found := false for _, id := range user.Orgs { if user.ActiveOrg.Id == id { @@ -2751,7 +2533,7 @@ func fixUserOrg(ctx context.Context, user *User) *User { continue } - org, err := getOrg(ctx, orgId) + org, err := shuffle.GetOrg(ctx, orgId) if err != nil { log.Printf("Error getting org %s", orgId) continue @@ -2768,10 +2550,10 @@ func fixUserOrg(ctx context.Context, user *User) *User { } if userFound { - user.PrivateApps = []WorkflowApp{} - user.Executions = ExecutionInfo{} - user.Limits = UserLimits{} - user.Authentication = []UserAuth{} + user.PrivateApps = []shuffle.WorkflowApp{} + user.Executions = shuffle.ExecutionInfo{} + user.Limits = shuffle.UserLimits{} + user.Authentication = []shuffle.UserAuth{} org.Users[orgIndex] = *user } else { @@ -2955,7 +2737,7 @@ func handleSetHook(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -3197,15 +2979,15 @@ func setSpecificSchedule(resp http.ResponseWriter, request *http.Request) { return } -func getSchedule(ctx context.Context, schedulename string) (*ScheduleOld, error) { - key := datastore.NameKey("schedules", strings.ToLower(schedulename), nil) - curUser := &ScheduleOld{} - if err := dbclient.Get(ctx, key, curUser); err != nil { - return &ScheduleOld{}, err - } - - return curUser, nil -} +//func GetSchedule(ctx context.Context, schedulename string) (*ScheduleOld, error) { +// key := datastore.NameKey("schedules", strings.ToLower(schedulename), nil) +// curUser := &ScheduleOld{} +// if err := dbclient.Get(ctx, key, curUser); err != nil { +// return &ScheduleOld{}, err +// } +// +// return curUser, nil +//} func getSpecificWebhook(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) @@ -3234,7 +3016,7 @@ func getSpecificWebhook(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() // FIXME: Schedule = trigger? - schedule, err := getSchedule(ctx, workflowId) + schedule, err := shuffle.GetSchedule(ctx, workflowId) if err != nil { log.Printf("Failed setting schedule: %s", err) resp.WriteHeader(401) @@ -3265,7 +3047,7 @@ func handleDeleteSchedule(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -3476,7 +3258,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { for _, item := range hook.Workflows { //log.Printf("Running webhook for workflow %s with startnode %s", item, hook.Start) - workflow := Workflow{ + workflow := shuffle.Workflow{ ID: "", } @@ -3573,7 +3355,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -3653,7 +3435,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { if requestdata.Environment == "cloud" { // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c log.Printf("[INFO] Should START a cloud webhook for url %s for startnode %s", currentUrl, startNode) - org, err := getOrg(ctx, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed finding org %s: %s", org.Id, err) return @@ -3730,7 +3512,7 @@ func sendHookResult(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -3798,7 +3580,7 @@ func handleGetHook(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -3886,7 +3668,7 @@ func getSpecificSchedule(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - schedule, err := getSchedule(ctx, workflowId) + schedule, err := shuffle.GetSchedule(ctx, workflowId) if err != nil { log.Printf("Failed getting schedule: %s", err) resp.WriteHeader(401) @@ -3953,7 +3735,7 @@ func executeSchedule(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() log.Printf("[INFO] EXECUTING %s!", workflowId) - idConfig, err := getSchedule(ctx, workflowId) + idConfig, err := shuffle.GetSchedule(ctx, workflowId) if err != nil { log.Printf("Error getting schedule: %s", err) resp.WriteHeader(401) @@ -4064,7 +3846,7 @@ func uploadWorkflowResult(resp http.ResponseWriter, request *http.Request) { // FIXME - validate ID as well ctx := context.Background() - schedule, err := getSchedule(ctx, workflowId) + schedule, err := shuffle.GetSchedule(ctx, workflowId) if err != nil { log.Printf("Failed setting schedule %s: %s", workflowId, err) resp.WriteHeader(401) @@ -4496,7 +4278,7 @@ func handleGetallHooks(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -4566,7 +4348,7 @@ func findAvailablePorts(startRange int64, endRange int64) string { } func handleSendalert(resp http.ResponseWriter, request *http.Request) { - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in sendalert: %s", err) resp.WriteHeader(401) @@ -4903,7 +4685,7 @@ func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { return } - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in getting specific workflow: %s", err) resp.WriteHeader(401) @@ -4953,7 +4735,7 @@ func getOpenapi(resp http.ResponseWriter, request *http.Request) { } // Just here to verify that the user is logged in - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in validate swagger: %s", err) resp.WriteHeader(401) @@ -4981,7 +4763,7 @@ func getOpenapi(resp http.ResponseWriter, request *http.Request) { // FIXME - FIX AUTH WITH APP ctx := context.Background() - //_, err = getApp(ctx, id) + //_, err = shuffle.GetApp(ctx, id) //if err == nil { // log.Println("You're supposed to be able to continue now.") //} @@ -5014,7 +4796,7 @@ func echoOpenapiData(resp http.ResponseWriter, request *http.Request) { } // Just here to verify that the user is logged in - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in validate swagger: %s", err) resp.WriteHeader(401) @@ -5201,7 +4983,7 @@ func validateSwagger(resp http.ResponseWriter, request *http.Request) { } // Just here to verify that the user is logged in - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in validate swagger: %s", err) resp.WriteHeader(401) @@ -5394,7 +5176,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { } log.Printf("[INFO] SETTING APP TO LIVE!!!") - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in verify swagger: %s", err) resp.WriteHeader(401) @@ -5431,7 +5213,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { if test.Editing { // Quick verification test ctx := context.Background() - app, err := getApp(ctx, test.Id) + app, err := shuffle.GetApp(ctx, test.Id) if err != nil { log.Printf("Error getting app when editing: %s", app.Name) resp.WriteHeader(401) @@ -5476,7 +5258,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { swagger.Info.Title = strings.Replace(swagger.Info.Title, " ", "_", -1) } - basePath, err := buildStructure(swagger, newmd5) + basePath, err := shuffle.BuildStructure(swagger, newmd5) if err != nil { log.Printf("Failed to build base structure: %s", err) resp.WriteHeader(500) @@ -5485,7 +5267,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { } //log.Printf("Should generate yaml") - swagger, api, pythonfunctions, err := generateYaml(swagger, newmd5) + swagger, api, pythonfunctions, err := shuffle.GenerateYaml(swagger, newmd5) if err != nil { log.Printf("Failed building and generating yaml: %s", err) resp.WriteHeader(500) @@ -5495,7 +5277,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { // FIXME: CHECK IF SAME NAME AS NORMAL APP // Can't overwrite existing normal app - workflowApps, err := getAllWorkflowApps(ctx, 500) + workflowApps, err := shuffle.GetAllWorkflowApps(ctx, 500) if err != nil { log.Printf("Failed getting all workflow apps from database to verify: %s", err) resp.WriteHeader(401) @@ -5515,7 +5297,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { api.Owner = user.Id - err = dumpApi(basePath, api) + err = shuffle.DumpApi(basePath, api) if err != nil { log.Printf("Failed dumping yaml: %s", err) resp.WriteHeader(500) @@ -5526,7 +5308,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { identifier := fmt.Sprintf("%s-%s", swagger.Info.Title, newmd5) classname := strings.Replace(identifier, " ", "", -1) classname = strings.Replace(classname, "-", "", -1) - parsedCode, err := dumpPython(basePath, classname, swagger.Info.Version, pythonfunctions) + parsedCode, err := shuffle.DumpPython(basePath, classname, swagger.Info.Version, pythonfunctions) if err != nil { log.Printf("Failed dumping python: %s", err) resp.WriteHeader(500) @@ -5546,7 +5328,8 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { // 5. Upload as cloud function // 1. Upload the API to datastore - err = deployAppToDatastore(ctx, api) + err = shuffle.DeployAppToDatastore(ctx, api) + //func DeployAppToDatastore(ctx context.Context, workflowapp WorkflowApp, bucketName string) error { if err != nil { log.Printf("Failed adding app to db: %s", err) resp.WriteHeader(500) @@ -5555,7 +5338,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { } // 2. Get all the required code - appbase, staticBaseline, err := getAppbase() + appbase, staticBaseline, err := shuffle.GetAppbase() if err != nil { log.Printf("Failed getting appbase: %s", err) resp.WriteHeader(500) @@ -5564,17 +5347,17 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { } // Have to do some quick checks of the python code (: - _, parsedCode = formatAppfile(parsedCode) + _, parsedCode = shuffle.FormatAppfile(parsedCode) - fixedAppbase := fixAppbase(appbase) - runner := getRunner(classname) + fixedAppbase := shuffle.FixAppbase(appbase) + runner := shuffle.GetRunnerOnprem(classname) // 2. Put it together stitched := string(staticBaseline) + strings.Join(fixedAppbase, "\n") + parsedCode + string(runner) //log.Println(stitched) // 3. Zip and stream it directly in the directory - _, err = streamZipdata(ctx, identifier, stitched, "requests\nurllib3") + _, err = shuffle.StreamZipdata(ctx, identifier, stitched, "requests\nurllib3", "") if err != nil { log.Printf("[ERROR] Zipfile error: %s", err) resp.WriteHeader(500) @@ -5643,7 +5426,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { user.PrivateApps[foundNumber] = api } - err = setUser(ctx, &user) + err = shuffle.SetUser(ctx, &user) if err != nil { log.Printf("[ERROR] Failed adding verification for user %s: %s", user.Username, err) resp.WriteHeader(500) @@ -5804,7 +5587,7 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio ctx := context.Background() // 1. Get the workflow // 2. Execute it with the data - workflow, err := getWorkflow(ctx, workflowId) + workflow, err := shuffle.GetWorkflow(ctx, workflowId) if err != nil { return err } @@ -5813,12 +5596,12 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio _ = workflow parsedArgument := executionArgument - newExec := ExecutionRequest{ + newExec := shuffle.ExecutionRequest{ ExecutionSource: executionSource, ExecutionArgument: parsedArgument, } - var execution ExecutionRequest + var execution shuffle.ExecutionRequest err = json.Unmarshal([]byte(parsedArgument), &execution) if err == nil { //log.Printf("[INFO] FOUND EXEC %#v", execution) @@ -5853,7 +5636,7 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio Body: ioutil.NopCloser(bytes.NewReader(b)), } - _, _, err = handleExecution(workflowId, Workflow{}, newRequest) + _, _, err = handleExecution(workflowId, shuffle.Workflow{}, newRequest) return err } @@ -5974,7 +5757,7 @@ func handleCloudJob(job CloudSyncJob) error { return err } - _, _, err = handleExecution(job.PrimaryItemId, Workflow{}, newRequest) + _, _, err = handleExecution(job.PrimaryItemId, shuffle.Workflow{}, newRequest) if err != nil { log.Printf("Failed continuing workflow from cloud user_input: %s", err) return err @@ -5999,7 +5782,7 @@ func handleCloudJob(job CloudSyncJob) error { } */ - newResults := []ActionResult{} + newResults := []shuffle.ActionResult{} for _, result := range workflowExecution.Results { if result.Action.AppName == "User Input" && result.Result == "Waiting for user feedback based on configuration" { result.Status = "ABORTED" @@ -6026,7 +5809,7 @@ func handleCloudJob(job CloudSyncJob) error { } // Handles jobs from remote (cloud) -func remoteOrgJobController(org Org, body []byte) error { +func remoteOrgJobController(org shuffle.Org, body []byte) error { type retStruct struct { Success bool `json:"success"` Reason string `json:"reason"` @@ -6051,7 +5834,7 @@ func remoteOrgJobController(org Org, body []byte) error { log.Printf("[WARNING] STOPPING ORG SCHEDULE for: %s", org.Id) value.Lock() - org, err := getOrg(ctx, org.Id) + org, err := shuffle.GetOrg(ctx, org.Id) if err != nil { log.Printf("[WARNING] Failed finding org %s: %s", org.Id, err) return err @@ -6091,7 +5874,7 @@ func remoteOrgJobController(org Org, body []byte) error { return nil } -func remoteOrgJobHandler(org Org, interval int) error { +func remoteOrgJobHandler(org shuffle.Org, interval int) error { client := &http.Client{} syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync", syncUrl) req, err := http.NewRequest( @@ -6179,7 +5962,7 @@ func runInit(ctx context.Context) { setUsers := false orgQuery := datastore.NewQuery("Organizations") - var activeOrgs []Org + var activeOrgs []shuffle.Org _, err = dbclient.GetAll(ctx, orgQuery, &activeOrgs) if err != nil { log.Printf("Error getting organizations!") @@ -6194,11 +5977,11 @@ func runInit(ctx context.Context) { log.Printf(`No orgs. Setting org "default"`) orgSetupName := "default" orgId := uuid.NewV4().String() - newOrg := Org{ + newOrg := shuffle.Org{ Name: orgSetupName, Id: orgId, Org: orgSetupName, - Users: []User{}, + Users: []shuffle.User{}, Roles: []string{"admin", "user"}, CloudSync: false, } @@ -6220,15 +6003,15 @@ func runInit(ctx context.Context) { activeOrg := activeOrgs[0] q := datastore.NewQuery("Users") - var users []User + var users []shuffle.User _, err = dbclient.GetAll(ctx, q, &users) if err == nil { setOrgBool := false for _, user := range users { - newUser := User{ + newUser := shuffle.User{ Username: user.Username, Id: user.Id, - ActiveOrg: Org{ + ActiveOrg: shuffle.Org{ Id: activeOrg.Id, }, Orgs: []string{activeOrg.Id}, @@ -6272,13 +6055,13 @@ func runInit(ctx context.Context) { // Fix active users etc q := datastore.NewQuery("Users").Filter("active =", true) - var activeusers []User + var activeusers []shuffle.User _, err = dbclient.GetAll(ctx, q, &activeusers) if err != nil { log.Printf("Error getting users during init: %s", err) } else { q := datastore.NewQuery("Users") - var users []User + var users []shuffle.User _, err := dbclient.GetAll(ctx, q, &users) if len(activeusers) == 0 && len(users) > 0 { @@ -6298,13 +6081,13 @@ func runInit(ctx context.Context) { if len(user.Orgs) == 0 { defaultName := "default" user.Orgs = []string{defaultName} - user.ActiveOrg = Org{ + user.ActiveOrg = shuffle.Org{ Name: defaultName, Role: "user", } } - err = setUser(ctx, &user) + err = shuffle.SetUser(ctx, &user) if err != nil { log.Printf("Failed to reset user") } else { @@ -6325,7 +6108,7 @@ func runInit(ctx context.Context) { } else { apikey := os.Getenv("SHUFFLE_DEFAULT_APIKEY") - tmpOrg := Org{ + tmpOrg := shuffle.Org{ Name: "default", } err = createNewUser(username, password, "admin", apikey, tmpOrg) @@ -6348,7 +6131,7 @@ func runInit(ctx context.Context) { for _, user := range users { if user.ActiveOrg.Id == "" && len(user.Username) > 0 { user.ActiveOrg = activeOrgs[0] - err = setUser(ctx, &user) + err = shuffle.SetUser(ctx, &user) if err != nil { log.Printf("Failed updating user %s with org", user.Username) } else { @@ -6365,7 +6148,7 @@ func runInit(ctx context.Context) { count, err := getEnvironmentCount() if count == 0 && err == nil && len(activeOrgs) == 1 { log.Printf("Setting up environment with org %s", activeOrgs[0].Id) - item := Environment{ + item := shuffle.Environment{ Name: "Shuffle", Type: "onprem", OrgId: activeOrgs[0].Id, @@ -6378,7 +6161,7 @@ func runInit(ctx context.Context) { } } else if len(activeOrgs) == 1 { log.Printf("Setting up all environments with org %s", activeOrgs[0].Id) - var environments []Environment + var environments []shuffle.Environment q := datastore.NewQuery("Environments") _, err = dbclient.GetAll(ctx, q, &environments) if err == nil { @@ -6399,7 +6182,7 @@ func runInit(ctx context.Context) { // Fixing workflows to have real activeorg IDs if len(activeOrgs) == 1 { q := datastore.NewQuery("workflow").Limit(35) - var workflows []Workflow + var workflows []shuffle.Workflow _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { log.Printf("Error getting workflows in runinit: %s", err) @@ -6467,7 +6250,7 @@ func runInit(ctx context.Context) { } */ - var allworkflowapps []AppAuthenticationStorage + var allworkflowapps []shuffle.AppAuthenticationStorage q = datastore.NewQuery("workflowappauth") _, err = dbclient.GetAll(ctx, q, &allworkflowapps) if err == nil { @@ -6479,7 +6262,7 @@ func runInit(ctx context.Context) { //log.Printf("Should update auth for %#v!", item) item.OrgId = activeOrgs[0].Id - err = setWorkflowAppAuthDatastore(ctx, item, item.Id) + err = shuffle.SetWorkflowAppAuthDatastore(ctx, item, item.Id) if err != nil { log.Printf("Failed adding AUTH to org %s", activeOrgs[0].Id) } @@ -6567,7 +6350,7 @@ func runInit(ctx context.Context) { Body: ioutil.NopCloser(strings.NewReader(schedule.WrappedArgument)), } - _, _, err := handleExecution(schedule.WorkflowId, Workflow{}, request) + _, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request) if err != nil { log.Printf("Failed to execute %s: %s", schedule.WorkflowId, err) } @@ -6593,18 +6376,18 @@ func runInit(ctx context.Context) { // Getting apps to see if we should initialize a test log.Printf("Getting remote workflow apps") - workflowapps, err := getAllWorkflowApps(ctx, 500) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) if err != nil { log.Printf("Failed getting apps (runInit): %s", err) } else if err == nil && len(workflowapps) > 0 { - //getAllWorkflowApps(ctx context.Context) ([]WorkflowApp, error) { - var allworkflowapps []WorkflowApp + //shuffle.GetAllWorkflowApps(ctx context.Context) ([]WorkflowApp, error) { + var allworkflowapps []shuffle.WorkflowApp q := datastore.NewQuery("workflowapp") _, err := dbclient.GetAll(ctx, q, &allworkflowapps) if err == nil { for _, workflowapp := range allworkflowapps { if workflowapp.Edited == 0 { - err = setWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) + err = shuffle.SetWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) if err == nil { log.Printf("Updating time for workflowapp %s:%s", workflowapp.Name, workflowapp.AppVersion) } @@ -6693,7 +6476,7 @@ func runInit(ctx context.Context) { if len(workflowLocation) > 0 { log.Printf("Downloading WORKFLOWS from %s if no workflows - EXTRA workflows", workflowLocation) q := datastore.NewQuery("workflow").Limit(35) - var workflows []Workflow + var workflows []shuffle.Workflow _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { log.Printf("Error getting workflows: %s", err) @@ -6722,11 +6505,11 @@ func runInit(ctx context.Context) { log.Printf("[INFO] Finished INIT") } -func handleVerifyCloudsync(orgId string) (SyncFeatures, error) { +func handleVerifyCloudsync(orgId string) (shuffle.SyncFeatures, error) { ctx := context.Background() - org, err := getOrg(ctx, orgId) + org, err := shuffle.GetOrg(ctx, orgId) if err != nil { - return SyncFeatures{}, err + return shuffle.SyncFeatures{}, err } //r.HandleFunc("/api/v1/getorgs", handleGetOrgs).Methods("GET", "OPTIONS") @@ -6742,26 +6525,26 @@ func handleVerifyCloudsync(orgId string) (SyncFeatures, error) { req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey)) newresp, err := client.Do(req) if err != nil { - return SyncFeatures{}, err + return shuffle.SyncFeatures{}, err } respBody, err := ioutil.ReadAll(newresp.Body) if err != nil { - return SyncFeatures{}, err + return shuffle.SyncFeatures{}, err } responseData := retStruct{} err = json.Unmarshal(respBody, &responseData) if err != nil { - return SyncFeatures{}, err + return shuffle.SyncFeatures{}, err } if newresp.StatusCode != 200 { - return SyncFeatures{}, errors.New(fmt.Sprintf("Got status code %d when getting org remotely. Expected 200. Contact support.", newresp.StatusCode)) + return shuffle.SyncFeatures{}, errors.New(fmt.Sprintf("Got status code %d when getting org remotely. Expected 200. Contact support.", newresp.StatusCode)) } if !responseData.Success { - return SyncFeatures{}, errors.New(responseData.Reason) + return shuffle.SyncFeatures{}, errors.New(responseData.Reason) } return responseData.SyncFeatures, nil @@ -6769,7 +6552,7 @@ func handleVerifyCloudsync(orgId string) (SyncFeatures, error) { // Actually stops syncing with cloud for an org. // Disables potential schedules, removes environments, breaks workflows etc. -func handleStopCloudSync(syncUrl string, org Org) error { +func handleStopCloudSync(syncUrl string, org shuffle.Org) error { if len(org.SyncConfig.Apikey) == 0 { return errors.New(fmt.Sprintf("Couldn't find any sync key to disable org %s", org.Id)) } @@ -6814,8 +6597,8 @@ func handleStopCloudSync(syncUrl string, org Org) error { ctx := context.Background() org.CloudSync = false - org.SyncFeatures = SyncFeatures{} - org.SyncConfig = SyncConfig{} + org.SyncFeatures = shuffle.SyncFeatures{} + org.SyncConfig = shuffle.SyncConfig{} err = setOrg(ctx, org, org.Id) if err != nil { @@ -6824,7 +6607,7 @@ func handleStopCloudSync(syncUrl string, org Org) error { return errors.New(newerror) } - var environments []Environment + var environments []shuffle.Environment q := datastore.NewQuery("Environments").Filter("org_id =", org.Id) _, err = dbclient.GetAll(ctx, q, &environments) if err != nil { @@ -6922,7 +6705,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() - org, err := getOrg(ctx, tmpData.OrgId) + org, err := shuffle.GetOrg(ctx, tmpData.OrgId) if err != nil { log.Printf("[INFO] Organization doesn't exist: %s", err) resp.WriteHeader(401) @@ -7103,7 +6886,7 @@ func handleEditOrg(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in cloud setup: %s", err) resp.WriteHeader(401) @@ -7126,11 +6909,11 @@ func handleEditOrg(resp http.ResponseWriter, request *http.Request) { } type ReturnData struct { - Image string `json:"image" datastore:"image"` - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description"` - OrgId string `json:"org_id" datastore:"org_id"` - Defaults Defaults `json:"defaults" datastore:"defaults"` + Image string `json:"image" datastore:"image"` + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + OrgId string `json:"org_id" datastore:"org_id"` + Defaults shuffle.Defaults `json:"defaults" datastore:"defaults"` } var tmpData ReturnData @@ -7163,7 +6946,7 @@ func handleEditOrg(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - org, err := getOrg(ctx, tmpData.OrgId) + org, err := shuffle.GetOrg(ctx, tmpData.OrgId) if err != nil { log.Printf("Organization doesn't exist: %s", err) resp.WriteHeader(401) @@ -7228,7 +7011,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in cloud setup: %s", err) resp.WriteHeader(401) @@ -7251,9 +7034,9 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { } type ReturnData struct { - Apikey string `datastore:"apikey"` - Organization Org `datastore:"organization"` - Disable bool `datastore:"disable"` + Apikey string `datastore:"apikey"` + Organization shuffle.Org `datastore:"organization"` + Disable bool `datastore:"disable"` } var tmpData ReturnData @@ -7266,7 +7049,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - org, err := getOrg(ctx, tmpData.Organization.Id) + org, err := shuffle.GetOrg(ctx, tmpData.Organization.Id) if err != nil { log.Printf("Organization doesn't exist: %s", err) resp.WriteHeader(401) @@ -7411,7 +7194,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { org.CloudSync = true org.SyncFeatures = responseData.SyncFeatures - org.SyncConfig = SyncConfig{ + org.SyncConfig = shuffle.SyncConfig{ Apikey: responseData.SessionKey, Interval: responseData.IntervalSeconds, } @@ -7440,7 +7223,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { // 1. Find environment // 2. If cloud env found, enable it (un-archive) // 3. If it doesn't create it - var environments []Environment + var environments []shuffle.Environment q := datastore.NewQuery("Environments").Filter("org_id =", org.Id) _, err = dbclient.GetAll(ctx, q, &environments) if err == nil { @@ -7465,7 +7248,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { if !found { log.Printf("Env for cloud not found. Should add it!") - newEnv := Environment{ + newEnv := shuffle.Environment{ Name: "Cloud", Type: "cloud", Archived: false, @@ -7509,7 +7292,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { // return // } // -// user, err := handleApiAuthentication(resp, request) +// user, err := shuffle.HandleApiAuthentication(resp, request) // if err != nil { // log.Printf("Api authentication failed in cloud setup: %s", err) // resp.WriteHeader(401) @@ -7584,7 +7367,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { // return // } // -// org, err := getOrg(ctx, tmpData.OrgId) +// org, err := shuffle.GetOrg(ctx, tmpData.OrgId) // if err != nil { // log.Printf("Organization doesn't exist: %s", err) // resp.WriteHeader(401) @@ -7684,12 +7467,18 @@ func initHandlers() { ctx := context.Background() log.Printf("Starting Shuffle backend - initializing database connection") - // option.WithoutAuthentication - + requestCache = cache.New(5*time.Minute, 10*time.Minute) dbclient, err = datastore.NewClient(ctx, gceProject, option.WithGRPCDialOption(grpc.WithNoProxy())) if err != nil { panic(fmt.Sprintf("DBclient error during init: %s", err)) } + + //dbclient, err := shuffle.GetDatastoreClient(ctx, gceProject) + //if err != nil { + // panic(fmt.Sprintf("Error setting datastore connector: %s", err)) + //} + + _ = shuffle.RunInit(*dbclient, storage.Client{}, gceProject, "onprem", true) log.Printf("Finished Shuffle database init") go runInit(ctx) @@ -7815,12 +7604,12 @@ func initHandlers() { // PS: For cloud, this has to use cloud storage. // https://developer.box.com/reference/get-files-id-content/ // 1. Creating the "get file" option. Make it possible to run this in the frontend. - r.HandleFunc("/api/v1/files/{fileId}/content", handleGetFileContent).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/files/create", handleCreateFile).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/files/{fileId}/upload", handleUploadFile).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/files/{fileId}", handleGetFileMeta).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/files/{fileId}", handleDeleteFile).Methods("DELETE", "OPTIONS") - r.HandleFunc("/api/v1/files", handleGetFiles).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/files/{fileId}/content", shuffle.HandleGetFileContent).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/files/create", shuffle.HandleCreateFile).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/files/{fileId}/upload", shuffle.HandleUploadFile).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleGetFileMeta).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleDeleteFile).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/files", shuffle.HandleGetFiles).Methods("GET", "OPTIONS") // Trigger hmm r.HandleFunc("/api/v1/triggers/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS") diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go index b00f6c7a..3c03ea13 100644 --- a/backend/go-app/oauth2.go +++ b/backend/go-app/oauth2.go @@ -1,6 +1,8 @@ package main import ( + "github.com/frikky/shuffle-shared" + "bytes" "context" "encoding/json" @@ -423,32 +425,32 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { } // Should also update the user - Userdata, err := getUser(ctx, senderUser) + Userdata, err := shuffle.GetUser(ctx, senderUser) if err != nil { log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err) resp.WriteHeader(401) return } - Userdata.Authentication = append(Userdata.Authentication, UserAuth{ + Userdata.Authentication = append(Userdata.Authentication, shuffle.UserAuth{ Name: "Outlook", Description: "oauth2", Workflows: []string{trigger.WorkflowId}, Username: trigger.Username, - Fields: []UserAuthField{ - UserAuthField{ + Fields: []shuffle.UserAuthField{ + shuffle.UserAuthField{ Key: "trigger_id", Value: trigger.Id, }, - UserAuthField{ + shuffle.UserAuthField{ Key: "username", Value: trigger.Username, }, - UserAuthField{ + shuffle.UserAuthField{ Key: "code", Value: code, }, - UserAuthField{ + shuffle.UserAuthField{ Key: "type", Value: trigger.Type, }, @@ -456,7 +458,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { }) // Set apikey for the user if they don't have one - err = setUser(ctx, Userdata) + err = shuffle.SetUser(ctx, Userdata) if err != nil { log.Printf("Failed setting user data for %s: %s", Userdata.Username, err) resp.WriteHeader(401) @@ -636,7 +638,7 @@ func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in getting specific workflow: %s", err) resp.WriteHeader(401) @@ -714,7 +716,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflow, err := getWorkflow(ctx, workflowId) + workflow, err := shuffle.GetWorkflow(ctx, workflowId) if err != nil { log.Printf("Failed getting the workflow locally (outlook sub): %s", err) resp.WriteHeader(401) @@ -722,7 +724,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in outlook deploy: %s", err) resp.WriteHeader(401) @@ -808,7 +810,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { // 10 * 5 = 50 seconds. That's waaay too much :( if runningEnvironment != "cloud" { - org, err := getOrg(ctx, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed finding org %s: %s", org.Id, err) return @@ -1147,7 +1149,7 @@ func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) { Body: ioutil.NopCloser(bytes.NewReader(b)), } - workflow := Workflow{ + workflow := shuffle.Workflow{ ID: "", } @@ -1196,7 +1198,7 @@ func removeOutlookSubscription(outlookClient *http.Client, subscriptionId string // Remove AUTH // Remove function // Remove subscription -func handleOutlookSubRemoval(ctx context.Context, user User, workflowId, triggerId string) error { +func handleOutlookSubRemoval(ctx context.Context, user shuffle.User, workflowId, triggerId string) error { // 1. Get the auth for trigger // 2. Stop the subscription // 3. Remove the function @@ -1209,7 +1211,7 @@ func handleOutlookSubRemoval(ctx context.Context, user User, workflowId, trigger if runningEnvironment != "cloud" { log.Printf("[INFO] SHOULD STOP OUTLOOK SUB ONPREM SYNC WITH CLOUD for workflow ID %s", workflowId) - org, err := getOrg(ctx, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("[INFO] Failed finding org %s during outlook removal: %s", org.Id, err) return err @@ -1289,7 +1291,7 @@ func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflow, err := getWorkflow(ctx, workflowId) + workflow, err := shuffle.GetWorkflow(ctx, workflowId) if err != nil { log.Printf("Failed getting the workflow locally (delete outlook): %s", err) resp.WriteHeader(401) @@ -1297,7 +1299,7 @@ func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in outlook deploy: %s", err) resp.WriteHeader(401) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 8af8055e..10b96f19 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1,6 +1,8 @@ package main import ( + "github.com/frikky/shuffle-shared" + "bytes" "context" "encoding/json" @@ -29,7 +31,7 @@ import ( schedulerpb "google.golang.org/genproto/googleapis/cloud/scheduler/v1" newscheduler "github.com/carlescere/scheduler" - "github.com/getkin/kin-openapi/openapi3" + "github.com/frikky/kin-openapi/openapi3" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" @@ -63,435 +65,416 @@ var scheduledOrgs = map[string]*newscheduler.Job{} // }, //} -type ExecutionRequest struct { - ExecutionId string `json:"execution_id,omitempty"` - ExecutionArgument string `json:"execution_argument,omitempty"` - ExecutionSource string `json:"execution_source,omitempty"` - WorkflowId string `json:"workflow_id,omitempty"` - Environments []string `json:"environments,omitempty"` - Authorization string `json:"authorization,omitempty"` - Status string `json:"status,omitempty"` - Start string `json:"start,omitempty"` - Type string `json:"type,omitempty"` -} - -type SyncFeatures struct { - Webhook SyncData `json:"webhook" datastore:"webhook"` - Schedules SyncData `json:"schedules" datastore:"schedules"` - UserInput SyncData `json:"user_input" datastore:"user_input"` - SendMail SyncData `json:"send_mail" datastore:"send_mail"` - SendSms SyncData `json:"send_sms" datastore:"send_sms"` - Updates SyncData `json:"updates" datastore:"updates"` - Notifications SyncData `json:"notifications" datastore:"notifications"` - EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` - AppExecutions SyncData `json:"app_executions" datastore:"app_executions"` - WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` - Apps SyncData `json:"apps" datastore:"apps"` - Workflows SyncData `json:"workflows" datastore:"workflows"` - Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` - Authentication SyncData `json:"authentication" datastore:"authentication"` - Schedule SyncData `json:"schedule" datastore:"schedule"` -} - -type SyncData struct { - Active bool `json:"active" datastore:"active"` - Type string `json:"type,omitempty" datastore:"type"` - Name string `json:"name,omitempty" datastore:"name"` - Description string `json:"description,omitempty" datastore:"description"` - Limit int64 `json:"limit,omitempty" datastore:"limit"` - StartDate int64 `json:"start_date,omitempty" datastore:"start_date"` - EndDate int64 `json:"end_date,omitempty" datastore:"end_date"` - DataCollection int64 `json:"data_collection,omitempty" datastore:"data_collection"` -} - -type SyncConfig struct { - Interval int64 `json:"interval" datastore:"interval"` - Apikey string `json:"api_key" datastore:"api_key"` -} - -// Role is just used for feedback for a user -type Org struct { - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description"` - Image string `json:"image" datastore:"image,noindex"` - Id string `json:"id" datastore:"id"` - Org string `json:"org" datastore:"org"` - Users []User `json:"users" datastore:"users"` - Role string `json:"role" datastore:"role"` - Roles []string `json:"roles" datastore:"roles"` - CloudSync bool `json:"cloud_sync" datastore:"CloudSync"` - SyncConfig SyncConfig `json:"sync_config" datastore:"sync_config"` - SyncFeatures SyncFeatures `json:"sync_features" datastore:"sync_features"` - Subscriptions []PaymentSubscription `json:"subscriptions" datastore:"subscriptions"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` - Defaults Defaults `json:"defaults" datastore:"defaults"` -} - -type PaymentSubscription struct { - Active bool `json:"active" datastore:"active"` - Startdate int64 `json:"startdate" datastore:"startdate"` - CancellationDate int64 `json:"cancellationdate" datastore:"cancellationdate"` - Enddate int64 `json:"enddate" datastore:"enddate"` - Name string `json:"name" datastore:"name"` - Recurrence string `json:"recurrence" datastore:"recurrence"` - Reference string `json:"reference" datastore:"reference"` - Level string `json:"level" datastore:"level"` - Amount string `json:"amount" datastore:"amount"` - Currency string `json:"currency" datastore:"currency"` -} - -type Defaults struct { - AppDownloadRepo string `json:"app_download_repo" datastore:"app_download_repo"` - AppDownloadBranch string `json:"app_download_branch" datastore:"app_download_branch"` - WorkflowDownloadRepo string `json:"workflow_download_repo" datastore:"workflow_download_repo"` - WorkflowDownloadBranch string `json:"workflow_download_branch" datastore:"workflow_download_branch"` -} - -type AppAuthenticationStorage struct { - Active bool `json:"active" datastore:"active"` - Label string `json:"label" datastore:"label"` - Id string `json:"id" datastore:"id"` - App WorkflowApp `json:"app" datastore:"app,noindex"` - Fields []AuthenticationStore `json:"fields" datastore:"fields"` - Usage []AuthenticationUsage `json:"usage" datastore:"usage"` - WorkflowCount int64 `json:"workflow_count" datastore:"workflow_count"` - NodeCount int64 `json:"node_count" datastore:"node_count"` - OrgId string `json:"org_id" datastore:"org_id"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` - Defined bool `json:"defined" datastore:"defined"` -} - -type AuthenticationUsage struct { - WorkflowId string `json:"workflow_id" datastore:"workflow_id"` - Nodes []string `json:"nodes" datastore:"nodes"` -} - -// An app inside Shuffle -// Source string `json:"source" datastore:"soure" yaml:"source"` - downloadlocation -type WorkflowApp struct { - Name string `json:"name" yaml:"name" required:true datastore:"name"` - IsValid bool `json:"is_valid" yaml:"is_valid" required:true datastore:"is_valid"` - ID string `json:"id" yaml:"id,omitempty" required:false datastore:"id"` - Link string `json:"link" yaml:"link" required:false datastore:"link,noindex"` - AppVersion string `json:"app_version" yaml:"app_version" required:true datastore:"app_version"` - SharingConfig string `json:"sharing_config" yaml:"sharing_config" datastore:"sharing_config"` - Generated bool `json:"generated" yaml:"generated" required:false datastore:"generated"` - Downloaded bool `json:"downloaded" yaml:"downloaded" required:false datastore:"downloaded"` - Sharing bool `json:"sharing" yaml:"sharing" required:false datastore:"sharing"` - Verified bool `json:"verified" yaml:"verified" required:false datastore:"verified"` - Invalid bool `json:"invalid" yaml:"invalid" required:false datastore:"invalid"` - Activated bool `json:"activated" yaml:"activated" required:false datastore:"activated"` - Tested bool `json:"tested" yaml:"tested" required:false datastore:"tested"` - Owner string `json:"owner" datastore:"owner" yaml:"owner"` - Hash string `json:"hash" datastore:"hash" yaml:"hash"` // api.yaml+dockerfile+src/app.py for apps - PrivateID string `json:"private_id" yaml:"private_id" required:false datastore:"private_id"` - Description string `json:"description" datastore:"description,noindex" required:false yaml:"description"` - Environment string `json:"environment" datastore:"environment" required:true yaml:"environment"` - SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` - LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` - ContactInfo struct { - Name string `json:"name" datastore:"name" yaml:"name"` - Url string `json:"url" datastore:"url" yaml:"url"` - } `json:"contact_info" datastore:"contact_info" yaml:"contact_info" required:false` - ReferenceInfo struct { - DocumentationUrl string `json:"documentation_url" datastore:"documentation_url"` - GithubUrl string `json:"github_url" datastore:"github_url"` - } - FolderMount struct { - FolderMount bool `json:"folder_mount" datastore:"folder_mount"` - SourceFolder string `json:"source_folder" datastore:"source_folder"` - DestinationFolder string `json:"destination_folder" datastore:"destination_folder"` - } - Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions,noindex"` - Authentication Authentication `json:"authentication" yaml:"authentication" required:false datastore:"authentication"` - Tags []string `json:"tags" yaml:"tags" required:false datastore:"activated"` - Categories []string `json:"categories" yaml:"categories" required:false datastore:"categories"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` - LastRuntime int64 `json:"last_runtime" datastore:"last_runtime"` -} - -type WorkflowAppActionParameter struct { - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - ID string `json:"id" datastore:"id" yaml:"id,omitempty"` - Name string `json:"name" datastore:"name" yaml:"name"` - Example string `json:"example" datastore:"example,noindex" yaml:"example"` - Value string `json:"value" datastore:"value,noindex" yaml:"value,omitempty"` - Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` - Options []string `json:"options" datastore:"options" yaml:"options"` - ActionField string `json:"action_field" datastore:"action_field" yaml:"actionfield,omitempty"` - Variant string `json:"variant" datastore:"variant" yaml:"variant,omitempty"` - Required bool `json:"required" datastore:"required" yaml:"required"` - Configuration bool `json:"configuration" datastore:"configuration" yaml:"configuration"` - Tags []string `json:"tags" datastore:"tags" yaml:"tags"` - Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` - SkipMulticheck bool `json:"skip_multicheck" datastore:"skip_multicheck" yaml:"skip_multicheck"` - ValueReplace []Valuereplace `json:"value_replace" datastore:"value_replace,noindex" yaml:"value_replace,omitempty"` - UniqueToggled bool `json:"unique_toggled" datastore:"unique_toggled" yaml:"unique_toggled"` -} - -type Valuereplace struct { - Key string `json:"key" datastore:"key" yaml:"key"` - Value string `json:"value" datastore:"value" yaml:"value"` -} - -type SchemaDefinition struct { - Type string `json:"type" datastore:"type"` -} - -type WorkflowAppAction struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id" yaml:"id,omitempty"` - Name string `json:"name" datastore:"name"` - Label string `json:"label" datastore:"label"` - NodeType string `json:"node_type" datastore:"node_type"` - Environment string `json:"environment" datastore:"environment"` - Sharing bool `json:"sharing" datastore:"sharing"` - PrivateID string `json:"private_id" datastore:"private_id"` - AppID string `json:"app_id" datastore:"app_id"` - Tags []string `json:"tags" datastore:"tags" yaml:"tags"` - Authentication []AuthenticationStore `json:"authentication" datastore:"authentication,noindex" yaml:"authentication,omitempty"` - Tested bool `json:"tested" datastore:"tested" yaml:"tested"` - Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"` - ExecutionVariable struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"execution_variable" datastore:"execution_variables"` - Returns struct { - Description string `json:"description" datastore:"returns" yaml:"description,omitempty"` - Example string `json:"example" datastore:"example,noindex" yaml:"example"` - ID string `json:"id" datastore:"id" yaml:"id,omitempty"` - Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` - } `json:"returns" datastore:"returns"` - AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` - Example string `json:"example,noindex" datastore:"example" yaml:"example"` - AuthNotRequired bool `json:"auth_not_required" datastore:"auth_not_required" yaml:"auth_not_required"` -} +//type ExecutionRequest struct { +// ExecutionId string `json:"execution_id,omitempty"` +// ExecutionArgument string `json:"execution_argument,omitempty"` +// ExecutionSource string `json:"execution_source,omitempty"` +// WorkflowId string `json:"workflow_id,omitempty"` +// Environments []string `json:"environments,omitempty"` +// Authorization string `json:"authorization,omitempty"` +// Status string `json:"status,omitempty"` +// Start string `json:"start,omitempty"` +// Type string `json:"type,omitempty"` +//} +// +//type SyncFeatures struct { +// Webhook SyncData `json:"webhook" datastore:"webhook"` +// Schedules SyncData `json:"schedules" datastore:"schedules"` +// UserInput SyncData `json:"user_input" datastore:"user_input"` +// SendMail SyncData `json:"send_mail" datastore:"send_mail"` +// SendSms SyncData `json:"send_sms" datastore:"send_sms"` +// Updates SyncData `json:"updates" datastore:"updates"` +// Notifications SyncData `json:"notifications" datastore:"notifications"` +// EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` +// AppExecutions SyncData `json:"app_executions" datastore:"app_executions"` +// WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` +// Apps SyncData `json:"apps" datastore:"apps"` +// Workflows SyncData `json:"workflows" datastore:"workflows"` +// Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` +// Authentication SyncData `json:"authentication" datastore:"authentication"` +// Schedule SyncData `json:"schedule" datastore:"schedule"` +//} +// +//type SyncData struct { +// Active bool `json:"active" datastore:"active"` +// Type string `json:"type,omitempty" datastore:"type"` +// Name string `json:"name,omitempty" datastore:"name"` +// Description string `json:"description,omitempty" datastore:"description"` +// Limit int64 `json:"limit,omitempty" datastore:"limit"` +// StartDate int64 `json:"start_date,omitempty" datastore:"start_date"` +// EndDate int64 `json:"end_date,omitempty" datastore:"end_date"` +// DataCollection int64 `json:"data_collection,omitempty" datastore:"data_collection"` +//} +// +//type SyncConfig struct { +// Interval int64 `json:"interval" datastore:"interval"` +// Apikey string `json:"api_key" datastore:"api_key"` +//} +// +//type PaymentSubscription struct { +// Active bool `json:"active" datastore:"active"` +// Startdate int64 `json:"startdate" datastore:"startdate"` +// CancellationDate int64 `json:"cancellationdate" datastore:"cancellationdate"` +// Enddate int64 `json:"enddate" datastore:"enddate"` +// Name string `json:"name" datastore:"name"` +// Recurrence string `json:"recurrence" datastore:"recurrence"` +// Reference string `json:"reference" datastore:"reference"` +// Level string `json:"level" datastore:"level"` +// Amount string `json:"amount" datastore:"amount"` +// Currency string `json:"currency" datastore:"currency"` +//} +// +//type Defaults struct { +// AppDownloadRepo string `json:"app_download_repo" datastore:"app_download_repo"` +// AppDownloadBranch string `json:"app_download_branch" datastore:"app_download_branch"` +// WorkflowDownloadRepo string `json:"workflow_download_repo" datastore:"workflow_download_repo"` +// WorkflowDownloadBranch string `json:"workflow_download_branch" datastore:"workflow_download_branch"` +//} +// +//type AppAuthenticationStorage struct { +// Active bool `json:"active" datastore:"active"` +// Label string `json:"label" datastore:"label"` +// Id string `json:"id" datastore:"id"` +// App WorkflowApp `json:"app" datastore:"app,noindex"` +// Fields []AuthenticationStore `json:"fields" datastore:"fields"` +// Usage []AuthenticationUsage `json:"usage" datastore:"usage"` +// WorkflowCount int64 `json:"workflow_count" datastore:"workflow_count"` +// NodeCount int64 `json:"node_count" datastore:"node_count"` +// OrgId string `json:"org_id" datastore:"org_id"` +// Created int64 `json:"created" datastore:"created"` +// Edited int64 `json:"edited" datastore:"edited"` +// Defined bool `json:"defined" datastore:"defined"` +//} +// +//type AuthenticationUsage struct { +// WorkflowId string `json:"workflow_id" datastore:"workflow_id"` +// Nodes []string `json:"nodes" datastore:"nodes"` +//} +// +//// An app inside Shuffle +//// Source string `json:"source" datastore:"soure" yaml:"source"` - downloadlocation +//type WorkflowApp struct { +// Name string `json:"name" yaml:"name" required:true datastore:"name"` +// IsValid bool `json:"is_valid" yaml:"is_valid" required:true datastore:"is_valid"` +// ID string `json:"id" yaml:"id,omitempty" required:false datastore:"id"` +// Link string `json:"link" yaml:"link" required:false datastore:"link,noindex"` +// AppVersion string `json:"app_version" yaml:"app_version" required:true datastore:"app_version"` +// SharingConfig string `json:"sharing_config" yaml:"sharing_config" datastore:"sharing_config"` +// Generated bool `json:"generated" yaml:"generated" required:false datastore:"generated"` +// Downloaded bool `json:"downloaded" yaml:"downloaded" required:false datastore:"downloaded"` +// Sharing bool `json:"sharing" yaml:"sharing" required:false datastore:"sharing"` +// Verified bool `json:"verified" yaml:"verified" required:false datastore:"verified"` +// Invalid bool `json:"invalid" yaml:"invalid" required:false datastore:"invalid"` +// Activated bool `json:"activated" yaml:"activated" required:false datastore:"activated"` +// Tested bool `json:"tested" yaml:"tested" required:false datastore:"tested"` +// Owner string `json:"owner" datastore:"owner" yaml:"owner"` +// Hash string `json:"hash" datastore:"hash" yaml:"hash"` // api.yaml+dockerfile+src/app.py for apps +// PrivateID string `json:"private_id" yaml:"private_id" required:false datastore:"private_id"` +// Description string `json:"description" datastore:"description,noindex" required:false yaml:"description"` +// Environment string `json:"environment" datastore:"environment" required:true yaml:"environment"` +// SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` +// LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` +// ContactInfo struct { +// Name string `json:"name" datastore:"name" yaml:"name"` +// Url string `json:"url" datastore:"url" yaml:"url"` +// } `json:"contact_info" datastore:"contact_info" yaml:"contact_info" required:false` +// ReferenceInfo struct { +// DocumentationUrl string `json:"documentation_url" datastore:"documentation_url"` +// GithubUrl string `json:"github_url" datastore:"github_url"` +// } +// FolderMount struct { +// FolderMount bool `json:"folder_mount" datastore:"folder_mount"` +// SourceFolder string `json:"source_folder" datastore:"source_folder"` +// DestinationFolder string `json:"destination_folder" datastore:"destination_folder"` +// } +// Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions,noindex"` +// Authentication Authentication `json:"authentication" yaml:"authentication" required:false datastore:"authentication"` +// Tags []string `json:"tags" yaml:"tags" required:false datastore:"activated"` +// Categories []string `json:"categories" yaml:"categories" required:false datastore:"categories"` +// Created int64 `json:"created" datastore:"created"` +// Edited int64 `json:"edited" datastore:"edited"` +// LastRuntime int64 `json:"last_runtime" datastore:"last_runtime"` +//} +// +//type WorkflowAppActionParameter struct { +// Description string `json:"description" datastore:"description,noindex" yaml:"description"` +// ID string `json:"id" datastore:"id" yaml:"id,omitempty"` +// Name string `json:"name" datastore:"name" yaml:"name"` +// Example string `json:"example" datastore:"example,noindex" yaml:"example"` +// Value string `json:"value" datastore:"value,noindex" yaml:"value,omitempty"` +// Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` +// Options []string `json:"options" datastore:"options" yaml:"options"` +// ActionField string `json:"action_field" datastore:"action_field" yaml:"actionfield,omitempty"` +// Variant string `json:"variant" datastore:"variant" yaml:"variant,omitempty"` +// Required bool `json:"required" datastore:"required" yaml:"required"` +// Configuration bool `json:"configuration" datastore:"configuration" yaml:"configuration"` +// Tags []string `json:"tags" datastore:"tags" yaml:"tags"` +// Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` +// SkipMulticheck bool `json:"skip_multicheck" datastore:"skip_multicheck" yaml:"skip_multicheck"` +// ValueReplace []Valuereplace `json:"value_replace" datastore:"value_replace,noindex" yaml:"value_replace,omitempty"` +// UniqueToggled bool `json:"unique_toggled" datastore:"unique_toggled" yaml:"unique_toggled"` +//} +// +//type Valuereplace struct { +// Key string `json:"key" datastore:"key" yaml:"key"` +// Value string `json:"value" datastore:"value" yaml:"value"` +//} +// +//type SchemaDefinition struct { +// Type string `json:"type" datastore:"type"` +//} +// +//type WorkflowAppAction struct { +// Description string `json:"description" datastore:"description,noindex"` +// ID string `json:"id" datastore:"id" yaml:"id,omitempty"` +// Name string `json:"name" datastore:"name"` +// Label string `json:"label" datastore:"label"` +// NodeType string `json:"node_type" datastore:"node_type"` +// Environment string `json:"environment" datastore:"environment"` +// Sharing bool `json:"sharing" datastore:"sharing"` +// PrivateID string `json:"private_id" datastore:"private_id"` +// AppID string `json:"app_id" datastore:"app_id"` +// Tags []string `json:"tags" datastore:"tags" yaml:"tags"` +// Authentication []AuthenticationStore `json:"authentication" datastore:"authentication,noindex" yaml:"authentication,omitempty"` +// Tested bool `json:"tested" datastore:"tested" yaml:"tested"` +// Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"` +// ExecutionVariable struct { +// Description string `json:"description" datastore:"description,noindex"` +// ID string `json:"id" datastore:"id"` +// Name string `json:"name" datastore:"name"` +// Value string `json:"value" datastore:"value,noindex"` +// } `json:"execution_variable" datastore:"execution_variables"` +// Returns struct { +// Description string `json:"description" datastore:"returns" yaml:"description,omitempty"` +// Example string `json:"example" datastore:"example,noindex" yaml:"example"` +// ID string `json:"id" datastore:"id" yaml:"id,omitempty"` +// Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` +// } `json:"returns" datastore:"returns"` +// AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` +// Example string `json:"example,noindex" datastore:"example" yaml:"example"` +// AuthNotRequired bool `json:"auth_not_required" datastore:"auth_not_required" yaml:"auth_not_required"` +//} // FIXME: Generate a callback authentication ID? // FIXME: Add org check .. -type WorkflowExecution struct { - Type string `json:"type" datastore:"type"` - Status string `json:"status" datastore:"status"` - Start string `json:"start" datastore:"start"` - ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` - ExecutionId string `json:"execution_id" datastore:"execution_id"` - ExecutionSource string `json:"execution_source" datastore:"execution_source"` - ExecutionParent string `json:"execution_parent" datastore:"execution_parent"` - ExecutionOrg string `json:"execution_org" datastore:"execution_org"` - WorkflowId string `json:"workflow_id" datastore:"workflow_id"` - LastNode string `json:"last_node" datastore:"last_node"` - Authorization string `json:"authorization" datastore:"authorization"` - Result string `json:"result" datastore:"result,noindex"` - StartedAt int64 `json:"started_at" datastore:"started_at"` - CompletedAt int64 `json:"completed_at" datastore:"completed_at"` - ProjectId string `json:"project_id" datastore:"project_id"` - Locations []string `json:"locations" datastore:"locations"` - Workflow Workflow `json:"workflow" datastore:"workflow,noindex"` - Results []ActionResult `json:"results" datastore:"results,noindex"` - ExecutionVariables []struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"execution_variables,omitempty" datastore:"execution_variables,omitempty"` - OrgId string `json:"org_id" datastore:"org_id"` -} +//type WorkflowExecution struct { +// Type string `json:"type" datastore:"type"` +// Status string `json:"status" datastore:"status"` +// Start string `json:"start" datastore:"start"` +// ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` +// ExecutionId string `json:"execution_id" datastore:"execution_id"` +// ExecutionSource string `json:"execution_source" datastore:"execution_source"` +// ExecutionParent string `json:"execution_parent" datastore:"execution_parent"` +// ExecutionOrg string `json:"execution_org" datastore:"execution_org"` +// WorkflowId string `json:"workflow_id" datastore:"workflow_id"` +// LastNode string `json:"last_node" datastore:"last_node"` +// Authorization string `json:"authorization" datastore:"authorization"` +// Result string `json:"result" datastore:"result,noindex"` +// StartedAt int64 `json:"started_at" datastore:"started_at"` +// CompletedAt int64 `json:"completed_at" datastore:"completed_at"` +// ProjectId string `json:"project_id" datastore:"project_id"` +// Locations []string `json:"locations" datastore:"locations"` +// Workflow Workflow `json:"workflow" datastore:"workflow,noindex"` +// Results []ActionResult `json:"results" datastore:"results,noindex"` +// ExecutionVariables []struct { +// Description string `json:"description" datastore:"description,noindex"` +// ID string `json:"id" datastore:"id"` +// Name string `json:"name" datastore:"name"` +// Value string `json:"value" datastore:"value,noindex"` +// } `json:"execution_variables,omitempty" datastore:"execution_variables,omitempty"` +// OrgId string `json:"org_id" datastore:"org_id"` +//} // This is for the nodes in a workflow, NOT the app action itself. -type Action struct { - AppName string `json:"app_name" datastore:"app_name"` - AppVersion string `json:"app_version" datastore:"app_version"` - AppID string `json:"app_id" datastore:"app_id"` - Errors []string `json:"errors" datastore:"errors"` - ID string `json:"id" datastore:"id"` - IsValid bool `json:"is_valid" datastore:"is_valid"` - IsStartNode bool `json:"isStartNode,omitempty" datastore:"isStartNode"` - Sharing bool `json:"sharing,omitempty" datastore:"sharing"` - PrivateID string `json:"private_id,omitempty" datastore:"private_id"` - Label string `json:"label,omitempty" datastore:"label"` - SmallImage string `json:"small_image,omitempty" datastore:"small_image,noindex" required:false yaml:"small_image"` - LargeImage string `json:"large_image,omitempty" datastore:"large_image,noindex" yaml:"large_image" required:false` - Environment string `json:"environment,omitempty" datastore:"environment"` - Name string `json:"name" datastore:"name"` - Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` - ExecutionVariable struct { - Description string `json:"description,omitempty" datastore:"description,noindex"` - ID string `json:"id,omitempty" datastore:"id"` - Name string `json:"name,omitempty" datastore:"name"` - Value string `json:"value,omitempty" datastore:"value,noindex"` - } `json:"execution_variable,omitempty" datastore:"execution_variable,omitempty"` - Position struct { - X float64 `json:"x,omitempty" datastore:"x"` - Y float64 `json:"y,omitempty" datastore:"y"` - } `json:"position,omitempty"` - Priority int `json:"priority,omitempty" datastore:"priority"` - AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` - Example string `json:"example,omitempty" datastore:"example,noindex"` - AuthNotRequired bool `json:"auth_not_required,omitempty" datastore:"auth_not_required" yaml:"auth_not_required"` - Category string `json:"category" datastore:"category"` -} +//type Action struct { +// AppName string `json:"app_name" datastore:"app_name"` +// AppVersion string `json:"app_version" datastore:"app_version"` +// AppID string `json:"app_id" datastore:"app_id"` +// Errors []string `json:"errors" datastore:"errors"` +// ID string `json:"id" datastore:"id"` +// IsValid bool `json:"is_valid" datastore:"is_valid"` +// IsStartNode bool `json:"isStartNode,omitempty" datastore:"isStartNode"` +// Sharing bool `json:"sharing,omitempty" datastore:"sharing"` +// PrivateID string `json:"private_id,omitempty" datastore:"private_id"` +// Label string `json:"label,omitempty" datastore:"label"` +// SmallImage string `json:"small_image,omitempty" datastore:"small_image,noindex" required:false yaml:"small_image"` +// LargeImage string `json:"large_image,omitempty" datastore:"large_image,noindex" yaml:"large_image" required:false` +// Environment string `json:"environment,omitempty" datastore:"environment"` +// Name string `json:"name" datastore:"name"` +// Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` +// ExecutionVariable struct { +// Description string `json:"description,omitempty" datastore:"description,noindex"` +// ID string `json:"id,omitempty" datastore:"id"` +// Name string `json:"name,omitempty" datastore:"name"` +// Value string `json:"value,omitempty" datastore:"value,noindex"` +// } `json:"execution_variable,omitempty" datastore:"execution_variable,omitempty"` +// Position struct { +// X float64 `json:"x,omitempty" datastore:"x"` +// Y float64 `json:"y,omitempty" datastore:"y"` +// } `json:"position,omitempty"` +// Priority int `json:"priority,omitempty" datastore:"priority"` +// AuthenticationId string `json:"authentication_id" datastore:"authentication_id"` +// Example string `json:"example,omitempty" datastore:"example,noindex"` +// AuthNotRequired bool `json:"auth_not_required,omitempty" datastore:"auth_not_required" yaml:"auth_not_required"` +// Category string `json:"category" datastore:"category"` +//} +// +//// Added environment for location to execute +//type Trigger struct { +// AppName string `json:"app_name" datastore:"app_name"` +// Description string `json:"description" datastore:"description,noindex"` +// LongDescription string `json:"long_description" datastore:"long_description"` +// Status string `json:"status" datastore:"status"` +// AppVersion string `json:"app_version" datastore:"app_version"` +// Errors []string `json:"errors" datastore:"errors"` +// ID string `json:"id" datastore:"id"` +// IsValid bool `json:"is_valid" datastore:"is_valid"` +// IsStartNode bool `json:"isStartNode" datastore:"isStartNode"` +// Label string `json:"label" datastore:"label"` +// SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` +// LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` +// Environment string `json:"environment" datastore:"environment"` +// TriggerType string `json:"trigger_type" datastore:"trigger_type"` +// Name string `json:"name" datastore:"name"` +// Tags []string `json:"tags" datastore:"tags" yaml:"tags"` +// Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` +// Position struct { +// X float64 `json:"x" datastore:"x"` +// Y float64 `json:"y" datastore:"y"` +// } `json:"position"` +// Priority int `json:"priority" datastore:"priority"` +//} +// +//type Branch struct { +// DestinationID string `json:"destination_id" datastore:"destination_id"` +// ID string `json:"id" datastore:"id"` +// SourceID string `json:"source_id" datastore:"source_id"` +// Label string `json:"label" datastore:"label"` +// HasError bool `json:"has_errors" datastore: "has_errors"` +// Conditions []Condition `json:"conditions" datastore: "conditions,noindex"` +//} +// +//// Same format for a lot of stuff +//type Condition struct { +// Condition WorkflowAppActionParameter `json:"condition" datastore:"condition"` +// Source WorkflowAppActionParameter `json:"source" datastore:"source"` +// Destination WorkflowAppActionParameter `json:"destination" datastore:"destination"` +//} +// +//type Schedule struct { +// Name string `json:"name" datastore:"name"` +// Frequency string `json:"frequency" datastore:"frequency"` +// ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` +// Id string `json:"id" datastore:"id"` +// OrgId string `json:"org_id" datastore:"org_id"` +// Environment string `json:"environment" datastore:"environment"` +//} -// Added environment for location to execute -type Trigger struct { - AppName string `json:"app_name" datastore:"app_name"` - Description string `json:"description" datastore:"description,noindex"` - LongDescription string `json:"long_description" datastore:"long_description"` - Status string `json:"status" datastore:"status"` - AppVersion string `json:"app_version" datastore:"app_version"` - Errors []string `json:"errors" datastore:"errors"` - ID string `json:"id" datastore:"id"` - IsValid bool `json:"is_valid" datastore:"is_valid"` - IsStartNode bool `json:"isStartNode" datastore:"isStartNode"` - Label string `json:"label" datastore:"label"` - SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` - LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` - Environment string `json:"environment" datastore:"environment"` - TriggerType string `json:"trigger_type" datastore:"trigger_type"` - Name string `json:"name" datastore:"name"` - Tags []string `json:"tags" datastore:"tags" yaml:"tags"` - Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` - Position struct { - X float64 `json:"x" datastore:"x"` - Y float64 `json:"y" datastore:"y"` - } `json:"position"` - Priority int `json:"priority" datastore:"priority"` -} +//type Workflow struct { +// Actions []Action `json:"actions" datastore:"actions,noindex"` +// Branches []Branch `json:"branches" datastore:"branches,noindex"` +// Triggers []Trigger `json:"triggers" datastore:"triggers,noindex"` +// Schedules []Schedule `json:"schedules" datastore:"schedules,noindex"` +// Configuration struct { +// ExitOnError bool `json:"exit_on_error" datastore:"exit_on_error"` +// StartFromTop bool `json:"start_from_top" datastore:"start_from_top"` +// } `json:"configuration,omitempty" datastore:"configuration"` +// Created int64 `json:"created" datastore:"created"` +// Edited int64 `json:"edited" datastore:"edited"` +// LastRuntime int64 `json:"last_runtime" datastore:"last_runtime"` +// Errors []string `json:"errors,omitempty" datastore:"errors"` +// Tags []string `json:"tags,omitempty" datastore:"tags"` +// ID string `json:"id" datastore:"id"` +// IsValid bool `json:"is_valid" datastore:"is_valid"` +// Name string `json:"name" datastore:"name"` +// Description string `json:"description" datastore:"description,noindex"` +// Start string `json:"start" datastore:"start"` +// Owner string `json:"owner" datastore:"owner"` +// Sharing string `json:"sharing" datastore:"sharing"` +// Org []Org `json:"org,omitempty" datastore:"org"` +// ExecutingOrg Org `json:"execution_org,omitempty" datastore:"execution_org"` +// OrgId string `json:"org_id,omitempty" datastore:"org_id"` +// WorkflowVariables []struct { +// Description string `json:"description" datastore:"description,noindex"` +// ID string `json:"id" datastore:"id"` +// Name string `json:"name" datastore:"name"` +// Value string `json:"value" datastore:"value,noindex"` +// } `json:"workflow_variables" datastore:"workflow_variables"` +// ExecutionVariables []struct { +// Description string `json:"description" datastore:"description,noindex"` +// ID string `json:"id" datastore:"id"` +// Name string `json:"name" datastore:"name"` +// Value string `json:"value" datastore:"value,noindex"` +// } `json:"execution_variables,omitempty" datastore:"execution_variables"` +// ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` +// PreviouslySaved bool `json:"previously_saved" datastore:"first_save"` +// Categories Categories `json:"categories" datastore:"categories"` +// ExampleArgument string `json:"example_argument" datastore:"example_argument,noindex"` +//} -type Branch struct { - DestinationID string `json:"destination_id" datastore:"destination_id"` - ID string `json:"id" datastore:"id"` - SourceID string `json:"source_id" datastore:"source_id"` - Label string `json:"label" datastore:"label"` - HasError bool `json:"has_errors" datastore: "has_errors"` - Conditions []Condition `json:"conditions" datastore: "conditions,noindex"` -} +//type Category struct { +// Name string `json:"name" datastore:"name"` +// Description string `json:"description" datastore:"description"` +// Count int64 `json:"count" datastore:"count"` +//} +// +//type Categories struct { +// SIEM Category `json:"siem" datastore:"siem"` +// Communication Category `json:"communication" datastore:"communication"` +// Assets Category `json:"assets" datastore:"assets"` +// Cases Category `json:"cases" datastore:"cases"` +// Network Category `json:"network" datastore:"network"` +// Intel Category `json:"intel" datastore:"intel"` +// EDR Category `json:"edr" datastore:"edr"` +// Other Category `json:"other" datastore:"other"` +//} -// Same format for a lot of stuff -type Condition struct { - Condition WorkflowAppActionParameter `json:"condition" datastore:"condition"` - Source WorkflowAppActionParameter `json:"source" datastore:"source"` - Destination WorkflowAppActionParameter `json:"destination" datastore:"destination"` -} - -type Schedule struct { - Name string `json:"name" datastore:"name"` - Frequency string `json:"frequency" datastore:"frequency"` - ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` - Id string `json:"id" datastore:"id"` - OrgId string `json:"org_id" datastore:"org_id"` - Environment string `json:"environment" datastore:"environment"` -} - -type Workflow struct { - Actions []Action `json:"actions" datastore:"actions,noindex"` - Branches []Branch `json:"branches" datastore:"branches,noindex"` - Triggers []Trigger `json:"triggers" datastore:"triggers,noindex"` - Schedules []Schedule `json:"schedules" datastore:"schedules,noindex"` - Configuration struct { - ExitOnError bool `json:"exit_on_error" datastore:"exit_on_error"` - StartFromTop bool `json:"start_from_top" datastore:"start_from_top"` - } `json:"configuration,omitempty" datastore:"configuration"` - Created int64 `json:"created" datastore:"created"` - Edited int64 `json:"edited" datastore:"edited"` - LastRuntime int64 `json:"last_runtime" datastore:"last_runtime"` - Errors []string `json:"errors,omitempty" datastore:"errors"` - Tags []string `json:"tags,omitempty" datastore:"tags"` - ID string `json:"id" datastore:"id"` - IsValid bool `json:"is_valid" datastore:"is_valid"` - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description,noindex"` - Start string `json:"start" datastore:"start"` - Owner string `json:"owner" datastore:"owner"` - Sharing string `json:"sharing" datastore:"sharing"` - Org []Org `json:"org,omitempty" datastore:"org"` - ExecutingOrg Org `json:"execution_org,omitempty" datastore:"execution_org"` - OrgId string `json:"org_id,omitempty" datastore:"org_id"` - WorkflowVariables []struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"workflow_variables" datastore:"workflow_variables"` - ExecutionVariables []struct { - Description string `json:"description" datastore:"description,noindex"` - ID string `json:"id" datastore:"id"` - Name string `json:"name" datastore:"name"` - Value string `json:"value" datastore:"value,noindex"` - } `json:"execution_variables,omitempty" datastore:"execution_variables"` - ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` - PreviouslySaved bool `json:"previously_saved" datastore:"first_save"` - Categories Categories `json:"categories" datastore:"categories"` - ExampleArgument string `json:"example_argument" datastore:"example_argument,noindex"` -} - -type Category struct { - Name string `json:"name" datastore:"name"` - Description string `json:"description" datastore:"description"` - Count int64 `json:"count" datastore:"count"` -} - -type Categories struct { - SIEM Category `json:"siem" datastore:"siem"` - Communication Category `json:"communication" datastore:"communication"` - Assets Category `json:"assets" datastore:"assets"` - Cases Category `json:"cases" datastore:"cases"` - Network Category `json:"network" datastore:"network"` - Intel Category `json:"intel" datastore:"intel"` - EDR Category `json:"edr" datastore:"edr"` - Other Category `json:"other" datastore:"other"` -} - -type ActionResult struct { - Action Action `json:"action" datastore:"action,noindex"` - ExecutionId string `json:"execution_id" datastore:"execution_id"` - Authorization string `json:"authorization" datastore:"authorization"` - Result string `json:"result" datastore:"result,noindex"` - StartedAt int64 `json:"started_at" datastore:"started_at"` - CompletedAt int64 `json:"completed_at" datastore:"completed_at"` - Status string `json:"status" datastore:"status"` -} - -type Authentication struct { - Required bool `json:"required" datastore:"required" yaml:"required" ` - Parameters []AuthenticationParams `json:"parameters" datastore:"parameters" yaml:"parameters"` -} - -type AuthenticationParams struct { - Description string `json:"description" datastore:"description,noindex" yaml:"description"` - ID string `json:"id" datastore:"id" yaml:"id"` - Name string `json:"name" datastore:"name" yaml:"name"` - Example string `json:"example" datastore:"example,noindex" yaml:"example"` - Value string `json:"value,omitempty" datastore:"value,noindex" yaml:"value"` - Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` - Required bool `json:"required" datastore:"required" yaml:"required"` - In string `json:"in" datastore:"in" yaml:"in"` - Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` - Scheme string `json:"scheme" datastore:"scheme" yaml:"scheme"` // Deprecated -} - -type AuthenticationStore struct { - Key string `json:"key" datastore:"key"` - Value string `json:"value" datastore:"value,noindex"` -} - -type ExecutionRequestWrapper struct { - Data []ExecutionRequest `json:"data"` -} - -type AppExecutionExample struct { - AppName string `json:"app_name" datastore:"app_name"` - AppVersion string `json:"app_version" datastore:"app_version"` - AppAction string `json:"app_action" datastore:"app_action"` - AppId string `json:"app_id" datastore:"app_id"` - ExampleId string `json:"example_id" datastore:"example_id"` - SuccessExamples []string `json:"success_examples" datastore:"success_examples,noindex"` - FailureExamples []string `json:"failure_examples" datastore:"failure_examples,noindex"` -} +//type ActionResult struct { +// Action Action `json:"action" datastore:"action,noindex"` +// ExecutionId string `json:"execution_id" datastore:"execution_id"` +// Authorization string `json:"authorization" datastore:"authorization"` +// Result string `json:"result" datastore:"result,noindex"` +// StartedAt int64 `json:"started_at" datastore:"started_at"` +// CompletedAt int64 `json:"completed_at" datastore:"completed_at"` +// Status string `json:"status" datastore:"status"` +//} +// +//type Authentication struct { +// Required bool `json:"required" datastore:"required" yaml:"required" ` +// Parameters []AuthenticationParams `json:"parameters" datastore:"parameters" yaml:"parameters"` +//} +// +//type AuthenticationParams struct { +// Description string `json:"description" datastore:"description,noindex" yaml:"description"` +// ID string `json:"id" datastore:"id" yaml:"id"` +// Name string `json:"name" datastore:"name" yaml:"name"` +// Example string `json:"example" datastore:"example,noindex" yaml:"example"` +// Value string `json:"value,omitempty" datastore:"value,noindex" yaml:"value"` +// Multiline bool `json:"multiline" datastore:"multiline" yaml:"multiline"` +// Required bool `json:"required" datastore:"required" yaml:"required"` +// In string `json:"in" datastore:"in" yaml:"in"` +// Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"` +// Scheme string `json:"scheme" datastore:"scheme" yaml:"scheme"` // Deprecated +//} +// +//type AuthenticationStore struct { +// Key string `json:"key" datastore:"key"` +// Value string `json:"value" datastore:"value,noindex"` +//} +// +//type ExecutionRequestWrapper struct { +// Data []ExecutionRequest `json:"data"` +//} +// +//type AppExecutionExample struct { +// AppName string `json:"app_name" datastore:"app_name"` +// AppVersion string `json:"app_version" datastore:"app_version"` +// AppAction string `json:"app_action" datastore:"app_action"` +// AppId string `json:"app_id" datastore:"app_id"` +// ExampleId string `json:"example_id" datastore:"example_id"` +// SuccessExamples []string `json:"success_examples" datastore:"success_examples,noindex"` +// FailureExamples []string `json:"failure_examples" datastore:"failure_examples,noindex"` +//} // This might be... a bit off, but that's fine :) // This might also be stupid, as we want timelines and such @@ -551,7 +534,7 @@ func increaseStatisticsField(ctx context.Context, fieldname, id string, amount i return nil } -func setWorkflowQueue(ctx context.Context, executionRequest ExecutionRequest, env string) error { +func setWorkflowQueue(ctx context.Context, executionRequest shuffle.ExecutionRequest, env string) error { orgKey := fmt.Sprintf("workflowqueue-%s", env) key := datastore.NameKey(orgKey, executionRequest.ExecutionId, nil) @@ -577,16 +560,16 @@ func setWorkflowQueue(ctx context.Context, executionRequest ExecutionRequest, en // return nil //} -func getWorkflowQueue(ctx context.Context, id string) (ExecutionRequestWrapper, error) { +func getWorkflowQueue(ctx context.Context, id string) (shuffle.ExecutionRequestWrapper, error) { orgId := fmt.Sprintf("workflowqueue-%s", id) q := datastore.NewQuery(orgId).Limit(10) - executions := []ExecutionRequest{} + executions := []shuffle.ExecutionRequest{} _, err := dbclient.GetAll(ctx, q, &executions) if err != nil { - return ExecutionRequestWrapper{}, err + return shuffle.ExecutionRequestWrapper{}, err } - return ExecutionRequestWrapper{Data: executions}, nil + return shuffle.ExecutionRequestWrapper{Data: executions}, nil //key := datastore.NameKey("workflowqueue", id, nil) //executions := ExecutionRequestWrapper{} @@ -660,7 +643,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode Body: ioutil.NopCloser(strings.NewReader(bodyWrapper)), } - _, _, err := handleExecution(workflowId, Workflow{ExecutingOrg: Org{Id: orgId}}, request) + _, _, err := handleExecution(workflowId, shuffle.Workflow{ExecutingOrg: shuffle.Org{Id: orgId}}, request) if err != nil { log.Printf("Failed to execute %s: %s", workflowId, err) } @@ -747,7 +730,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque // Getting from the request //log.Println(string(body)) - var removeExecutionRequests ExecutionRequestWrapper + var removeExecutionRequests shuffle.ExecutionRequestWrapper err = json.Unmarshal(body, &removeExecutionRequests) if err != nil { log.Printf("Failed executionrequest in queue unmarshaling: %s", err) @@ -831,7 +814,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { } if len(executionRequests.Data) == 0 { - executionRequests.Data = []ExecutionRequest{} + executionRequests.Data = []shuffle.ExecutionRequest{} } else { log.Printf("[INFO] Executionrequests (%s): %d", id, len(executionRequests.Data)) } @@ -861,7 +844,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { return } - var actionResult ActionResult + var actionResult shuffle.ActionResult err = json.Unmarshal(body, &actionResult) if err != nil { log.Printf("Failed ActionResult unmarshaling: %s", err) @@ -871,7 +854,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflowExecution, err := getWorkflowExecution(ctx, actionResult.ExecutionId) + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) if err != nil { //log.Printf("Failed getting execution (streamresult) %s: %s", actionResult.ExecutionId, err) resp.WriteHeader(401) @@ -901,7 +884,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { // Finds the child nodes of a node in execution and returns them // Used if e.g. a node in a branch is exited, and all children have to be stopped -func findChildNodes(workflowExecution WorkflowExecution, nodeId string) []string { +func findChildNodes(workflowExecution shuffle.WorkflowExecution, nodeId string) []string { //log.Printf("\nNODE TO FIX: %s\n\n", nodeId) allChildren := []string{nodeId} @@ -955,14 +938,14 @@ func validateNewWorkerExecution(body []byte) error { //} ctx := context.Background() - var execution WorkflowExecution + var execution shuffle.WorkflowExecution err := json.Unmarshal(body, &execution) if err != nil { log.Printf("[WARNING] Failed execution unmarshaling: %s", err) return err } - baseExecution, err := getWorkflowExecution(ctx, execution.ExecutionId) + baseExecution, err := shuffle.GetWorkflowExecution(ctx, execution.ExecutionId) if err != nil { log.Printf("[ERROR] Failed getting execution (workflowqueue) %s: %s", execution.ExecutionId, err) return err @@ -1044,7 +1027,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { //log.Printf("[WARNING] Handling other execution variant: %s", err) } - var actionResult ActionResult + var actionResult shuffle.ActionResult err = json.Unmarshal(body, &actionResult) if err != nil { log.Printf("Failed ActionResult unmarshaling: %s", err) @@ -1060,7 +1043,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // IF FAIL: Set executionstatus: abort or cancel ctx := context.Background() - workflowExecution, err := getWorkflowExecution(ctx, actionResult.ExecutionId) + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) if err != nil { log.Printf("[ERROR] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, err) resp.WriteHeader(401) @@ -1099,7 +1082,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { if actionResult.Status == "WAITING" && actionResult.Action.AppName == "User Input" { log.Printf("SHOULD WAIT A BIT AND RUN CLOUD STUFF WITH USER INPUT! WAITING!") - var trigger Trigger + var trigger shuffle.Trigger err = json.Unmarshal([]byte(actionResult.Result), &trigger) if err != nil { log.Printf("Failed unmarshaling actionresult for user input: %s", err) @@ -1152,9 +1135,9 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { } // Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times -func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workflowExecutionId string, actionResult ActionResult, resp http.ResponseWriter) { +func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workflowExecutionId string, actionResult shuffle.ActionResult, resp http.ResponseWriter) { // Should start a tx for the execution here - workflowExecution, err := getWorkflowExecution(ctx, workflowExecutionId) + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, workflowExecutionId) if err != nil { log.Printf("[ERROR] Failed getting execution cache: %s", err) resp.WriteHeader(401) @@ -1192,7 +1175,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl if actionResult.Status == "ABORTED" || actionResult.Status == "FAILURE" { dbSave = true - newResults := []ActionResult{} + newResults := []shuffle.ActionResult{} childNodes := []string{} if workflowExecution.Workflow.Configuration.ExitOnError { log.Printf("[WARNING] Actionresult is %s for node %s in %s. Should set workflowExecution and exit all running functions", actionResult.Status, actionResult.Action.ID, workflowExecution.ExecutionId) @@ -1214,7 +1197,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // 1. Find the action itself // 2. Create an actionresult - curAction := Action{ID: ""} + curAction := shuffle.Action{ID: ""} for _, action := range workflowExecution.Workflow.Actions { if action.ID == nodeId { curAction = action @@ -1258,7 +1241,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } if !skipNodeAdd { - newAction := Action{ + newAction := shuffle.Action{ AppName: curAction.AppName, AppVersion: curAction.AppVersion, Label: curAction.Label, @@ -1266,7 +1249,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl ID: curAction.ID, } - newResult := ActionResult{ + newResult := shuffle.ActionResult{ Action: newAction, ExecutionId: actionResult.ExecutionId, Authorization: actionResult.Authorization, @@ -1474,7 +1457,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } // FIXME - why isn't this how it works otherwise, wtf? - //workflow, err := getWorkflow(workflowExecution.Workflow.ID) + //workflow, err := shuffle.GetWorkflow(workflowExecution.Workflow.ID) //newActions := []Action{} //for _, action := range workflowExecution.Workflow.Actions { // log.Printf("Name: %s, Env: %s", action.Name, action.Environment) @@ -1489,7 +1472,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Result string `json:"result" datastore:"result,noindex"` // Arbitrary reduction size maxSize := 500000 - newResults := []ActionResult{} + newResults := []shuffle.ActionResult{} for _, item := range workflowExecution.Results { if len(item.Result) > maxSize { item.Result = "[ERROR] Result too large to handle (https://github.com/frikky/shuffle/issues/171)" @@ -1506,7 +1489,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Handled using cachhing, so actually pretty fast cacheKey := fmt.Sprintf("workflowexecution-%s", workflowExecution.ExecutionId) if value, found := requestCache.Get(cacheKey); found { - parsedValue := value.(*WorkflowExecution) + parsedValue := value.(*shuffle.WorkflowExecution) if len(parsedValue.Results) > 0 && len(parsedValue.Results) != resultLength { setExecution = false if attempts > 5 { @@ -1587,10 +1570,10 @@ func JSONCheck(str string) bool { return json.Unmarshal([]byte(str), &jsonStr) == nil } -func handleExecutionStatistics(execution WorkflowExecution) { +func handleExecutionStatistics(execution shuffle.WorkflowExecution) { // FIXME: CLEAN UP THE JSON THAT'S SAVED. // https://github.com/frikky/Shuffle/issues/172 - appResults := []AppExecutionExample{} + appResults := []shuffle.AppExecutionExample{} for _, result := range execution.Results { resultCheck := JSONCheck(result.Result) if !resultCheck { @@ -1625,7 +1608,7 @@ func handleExecutionStatistics(execution WorkflowExecution) { } else { // CREATE SuccessExamples or FailureExamples - executionExample := AppExecutionExample{ + executionExample := shuffle.AppExecutionExample{ AppName: result.Action.AppName, AppVersion: result.Action.AppVersion, AppAction: result.Action.Name, @@ -1672,7 +1655,7 @@ func getWorkflows(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in getworkflows: %s", err) resp.WriteHeader(401) @@ -1703,7 +1686,7 @@ func getWorkflows(resp http.ResponseWriter, request *http.Request) { q = q.Order("-edited") - var workflows []Workflow + var workflows []shuffle.Workflow _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { @@ -1765,7 +1748,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -1781,7 +1764,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { return } - var workflow Workflow + var workflow shuffle.Workflow err = json.Unmarshal(body, &workflow) if err != nil { log.Printf("Failed unmarshaling: %s", err) @@ -1793,7 +1776,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.ID = uuid.NewV4().String() workflow.Owner = user.Id workflow.Sharing = "private" - user.ActiveOrg.Users = []User{} + user.ActiveOrg.Users = []shuffle.User{} workflow.ExecutingOrg = user.ActiveOrg workflow.OrgId = user.ActiveOrg.Id //log.Printf("TRIGGERS: %d", len(workflow.Triggers)) @@ -1805,19 +1788,19 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { //} if len(workflow.Actions) == 0 { - workflow.Actions = []Action{} + workflow.Actions = []shuffle.Action{} } if len(workflow.Branches) == 0 { - workflow.Branches = []Branch{} + workflow.Branches = []shuffle.Branch{} } if len(workflow.Triggers) == 0 { - workflow.Triggers = []Trigger{} + workflow.Triggers = []shuffle.Trigger{} } if len(workflow.Errors) == 0 { workflow.Errors = []string{} } - newActions := []Action{} + newActions := []shuffle.Action{} for _, action := range workflow.Actions { if action.Environment == "" { //action.Environment = baseEnvironment @@ -1833,11 +1816,11 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { //log.Printf("APPENDING NEW APP FOR NEW WORKFLOW") // Adds the Testing app if it's a new workflow - workflowapps, err := getAllWorkflowApps(ctx, 500) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) if err == nil { // FIXME: Add real env envName := "Shuffle" - environments, err := getEnvironments(ctx, user.ActiveOrg.Id) + environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id) if err == nil { for _, env := range environments { if env.Default { @@ -1851,11 +1834,11 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { if item.Name == "Testing" && item.AppVersion == "1.0.0" { nodeId := "40447f30-fa44-4a4f-a133-4ee710368737" workflow.Start = nodeId - newActions = append(newActions, Action{ + newActions = append(newActions, shuffle.Action{ Label: "Start node", Name: "hello_world", Environment: envName, - Parameters: []WorkflowAppActionParameter{}, + Parameters: []shuffle.WorkflowAppActionParameter{}, Position: struct { X float64 "json:\"x,omitempty\" datastore:\"x\"" Y float64 "json:\"y,omitempty\" datastore:\"y\"" @@ -1882,7 +1865,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME: Check if they require authentication and if they exist locally //log.Printf("\n\nSHOULD VALIDATE AUTHENTICATION") //AuthenticationId string `json:"authentication_id,omitempty" datastore:"authentication_id"` - //allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) + //allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) //if err == nil { // log.Printf("AUTH: %#v", allAuths) // for _, action := range newActions { @@ -1891,7 +1874,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { //} } - workflow.Actions = []Action{} + workflow.Actions = []shuffle.Action{} for _, item := range workflow.Actions { oldId := item.ID sourceIndexes := []int{} @@ -1918,7 +1901,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { newActions = append(newActions, item) } - newTriggers := []Trigger{} + newTriggers := []shuffle.Trigger{} for _, item := range workflow.Triggers { oldId := item.ID sourceIndexes := []int{} @@ -1946,7 +1929,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { newTriggers = append(newTriggers, item) } - newSchedules := []Schedule{} + newSchedules := []shuffle.Schedule{} for _, item := range workflow.Schedules { item.Id = uuid.NewV4().String() newSchedules = append(newSchedules, item) @@ -1991,7 +1974,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in deleting workflow: %s", err) resp.WriteHeader(401) @@ -2019,7 +2002,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflow, err := getWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Failed getting the workflow locally (delete workflow): %s", err) resp.WriteHeader(401) @@ -2084,7 +2067,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { } // Adds app auth tracking -func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add bool) error { +func updateAppAuth(auth shuffle.AppAuthenticationStorage, workflowId, nodeId string, add bool) error { workflowFound := false workflowIndex := 0 nodeFound := false @@ -2108,7 +2091,7 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add updateAuth := false if !workflowFound && add { log.Printf("[INFO] Adding workflow things to auth!") - usageItem := AuthenticationUsage{ + usageItem := shuffle.AuthenticationUsage{ WorkflowId: workflowId, Nodes: []string{nodeId}, } @@ -2127,7 +2110,7 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add if updateAuth { log.Printf("[INFO] Updating auth!") ctx := context.Background() - err := setWorkflowAppAuthDatastore(ctx, auth, auth.Id) + err := shuffle.SetWorkflowAppAuthDatastore(ctx, auth, auth.Id) if err != nil { log.Printf("Failed setting up app auth %s: %s", auth.Id, err) return err @@ -2138,7 +2121,7 @@ func updateAppAuth(auth AppAuthenticationStorage, workflowId, nodeId string, add } // Identifies what a category defined really is -func handleCategoryIncrease(categories Categories, action Action, workflowapps []WorkflowApp) Categories { +func handleCategoryIncrease(categories shuffle.Categories, action shuffle.Action, workflowapps []shuffle.WorkflowApp) shuffle.Categories { if action.Category == "" { appName := action.AppName for _, app := range workflowapps { @@ -2191,7 +2174,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } //log.Println("Start") - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in edit workflow: %s", userErr) resp.WriteHeader(401) @@ -2222,7 +2205,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // Here to check access rights ctx := context.Background() - tmpworkflow, err := getWorkflow(ctx, fileId) + tmpworkflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Failed getting the workflow locally (save workflow): %s", err) resp.WriteHeader(401) @@ -2247,7 +2230,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - var workflow Workflow + var workflow shuffle.Workflow err = json.Unmarshal([]byte(body), &workflow) //log.Printf(string(body)) if err != nil { @@ -2275,17 +2258,17 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { if len(workflow.ExecutingOrg.Id) == 0 { log.Printf("[INFO] Setting executing org for workflow") - user.ActiveOrg.Users = []User{} + user.ActiveOrg.Users = []shuffle.User{} workflow.ExecutingOrg = user.ActiveOrg } // FIXME - this shouldn't be necessary with proper API checks - newActions := []Action{} + newActions := []shuffle.Action{} allNodes := []string{} - workflow.Categories = Categories{} + workflow.Categories = shuffle.Categories{} //log.Printf("PRE APPS") - workflowapps, apperr := getAllWorkflowApps(ctx, 500) + workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 500) //log.Printf("Action: %#v", action.Authentication) for _, action := range workflow.Actions { @@ -2322,14 +2305,14 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { newActions = append(newActions, action) } - newTriggers := []Trigger{} + newTriggers := []shuffle.Trigger{} for _, trigger := range workflow.Triggers { log.Printf("[INFO] Trigger %s: %s", trigger.TriggerType, trigger.Status) // Check if it's actually running // FIXME: Do this for other triggers too if trigger.TriggerType == "SCHEDULE" && trigger.Status != "uninitialized" { - schedule, err := getSchedule(ctx, trigger.ID) + schedule, err := shuffle.GetSchedule(ctx, trigger.ID) if err != nil { trigger.Status = "stopped" } else if schedule.Id == "" { @@ -2344,7 +2327,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { if len(user.ApiKey) > 0 { apikey = user.ApiKey } else { - user, err = generateApikey(ctx, user) + user, err = shuffle.GenerateApikey(ctx, user) if err != nil { workflow.IsValid = false workflow.Errors = []string{"Trigger is missing a parameter: %s", param.Name} @@ -2448,13 +2431,13 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Triggers = newTriggers if len(workflow.Actions) == 0 { - workflow.Actions = []Action{} + workflow.Actions = []shuffle.Action{} } if len(workflow.Branches) == 0 { - workflow.Branches = []Branch{} + workflow.Branches = []shuffle.Branch{} } if len(workflow.Triggers) == 0 { - workflow.Triggers = []Trigger{} + workflow.Triggers = []shuffle.Trigger{} } if len(workflow.Errors) == 0 { workflow.Errors = []string{} @@ -2500,7 +2483,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { if len(foundNodes) != len(allNodes) || len(workflow.Actions) <= 0 { // This shit takes a few seconds lol if !workflow.IsValid { - oldworkflow, err := getWorkflow(ctx, fileId) + oldworkflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Workflow %s doesn't exist - oldworkflow.", fileId) if workflow.PreviouslySaved { @@ -2542,7 +2525,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // Have to do it like this to add the user's apps //log.Println("Apps set starting") //log.Printf("EXIT ON ERROR: %#v", workflow.Configuration.ExitOnError) - //workflowapps, apperr := getAllWorkflowApps(ctx, 500) + //workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 500) // Started getting the single apps, but if it's weird, this is faster // 1. Check workflow.Start @@ -2567,7 +2550,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } - allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) + allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) if userErr != nil { log.Printf("Api authentication failed in get all apps: %s", userErr) if workflow.PreviouslySaved { @@ -2579,8 +2562,8 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // Check every app action and param to see whether they exist //log.Printf("PRE ACTIONS 2") - allAuths, autherr := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - newActions = []Action{} + allAuths, autherr := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) + newActions = []shuffle.Action{} for _, action := range workflow.Actions { reservedApps := []string{ "0ca8887e-b4af-4e3e-887c-87e9d3bc3d3e", @@ -2629,7 +2612,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { if builtin { newActions = append(newActions, action) } else { - curapp := WorkflowApp{} + curapp := shuffle.WorkflowApp{} // FIXME - can this work with ONLY AppID? for _, app := range workflowapps { if app.ID == action.AppID { @@ -2659,7 +2642,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { //return } else { // Check tosee if the appaction is valid - curappaction := WorkflowAppAction{} + curappaction := shuffle.WorkflowAppAction{} for _, curAction := range curapp.Actions { if action.Name == curAction.Name { curappaction = curAction @@ -2685,7 +2668,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME - check all parameters to see if they're valid // Includes checking required fields - selectedAuth := AppAuthenticationStorage{} + selectedAuth := shuffle.AppAuthenticationStorage{} if len(action.AuthenticationId) > 0 && autherr == nil { for _, auth := range allAuths { if auth.Id == action.AuthenticationId { @@ -2695,7 +2678,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } - newParams := []WorkflowAppActionParameter{} + newParams := []shuffle.WorkflowAppActionParameter{} for _, param := range curappaction.Parameters { paramFound := false @@ -2765,7 +2748,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { if autherr == nil && len(workflowapps) > 0 && apperr == nil { //log.Printf("Setting actions") - actionFixing := []Action{} + actionFixing := []shuffle.Action{} appsAdded := []string{} for _, action := range newActions { setAuthentication := false @@ -2813,7 +2796,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME: Only o this IF there isn't another one for the app already if !authSet { //log.Printf("Validate if the app NEEDS auth or not") - outerapp := WorkflowApp{} + outerapp := shuffle.WorkflowApp{} for _, app := range workflowapps { if app.Name == action.AppName { outerapp = app @@ -2839,21 +2822,21 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME: Add app auth if !found { timeNow := int64(time.Now().Unix()) - authFields := []AuthenticationStore{} + authFields := []shuffle.AuthenticationStore{} for _, param := range outerapp.Authentication.Parameters { - authFields = append(authFields, AuthenticationStore{ + authFields = append(authFields, shuffle.AuthenticationStore{ Key: param.Name, Value: "", }) } - appAuth := AppAuthenticationStorage{ + appAuth := shuffle.AppAuthenticationStorage{ Active: true, Label: fmt.Sprintf("default_%s", outerapp.Name), Id: uuid.NewV4().String(), App: outerapp, Fields: authFields, - Usage: []AuthenticationUsage{}, + Usage: []shuffle.AuthenticationUsage{}, WorkflowCount: 0, NodeCount: 0, OrgId: user.ActiveOrg.Id, @@ -2861,7 +2844,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { Edited: timeNow, } - err = setWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) + err = shuffle.SetWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) if err != nil { log.Printf("Failed setting appauth for with name %s", appAuth.Label) } else { @@ -2876,7 +2859,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { //outerapp.Authentication.Required // Authentication Authentication `json:"authentication" yaml:"authentication" required:false datastore:"authentication"` - //workflowapps, apperr := getAllWorkflowApps(ctx, 100) + //workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 100) } } @@ -2886,8 +2869,8 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { newActions = actionFixing } else { log.Printf("FirstSave error: %s - %s", err, apperr) - //workflowapps, apperr := getAllWorkflowApps(ctx, 100) - //allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) + //workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 100) + //allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) } workflow.PreviouslySaved = true @@ -3022,7 +3005,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflowExecution, err := getWorkflowExecution(ctx, executionId) + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, executionId) if err != nil { log.Printf("[ERROR] Failed getting execution (abort) %s: %s", executionId, err) resp.WriteHeader(401) @@ -3041,7 +3024,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { if workflowExecution.Authorization != parsedKey { // FIXME: Check the execution if this fails. - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in abort workflow: %s", err) resp.WriteHeader(401) @@ -3074,7 +3057,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] Running shutdown of %s", workflowExecution.ExecutionId) lastResult := "" - newResults := []ActionResult{} + newResults := []shuffle.ActionResult{} // type ActionResult struct { for _, result := range workflowExecution.Results { if result.Status == "EXECUTING" { @@ -3116,7 +3099,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { } if len(workflowExecution.Results) == 0 || addResult { - newaction := Action{ + newaction := shuffle.Action{ ID: workflowExecution.Start, } @@ -3127,7 +3110,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { } } - workflowExecution.Results = append(workflowExecution.Results, ActionResult{ + workflowExecution.Results = append(workflowExecution.Results, shuffle.ActionResult{ Action: newaction, ExecutionId: workflowExecution.ExecutionId, Authorization: workflowExecution.Authorization, @@ -3144,7 +3127,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { if nodeok { nodeId := node[0] log.Printf("[INFO] Found abort node %s", nodeId) - newaction := Action{ + newaction := shuffle.Action{ ID: nodeId, } @@ -3155,7 +3138,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { } } - workflowExecution.Results = append(workflowExecution.Results, ActionResult{ + workflowExecution.Results = append(workflowExecution.Results, shuffle.ActionResult{ Action: newaction, ExecutionId: workflowExecution.ExecutionId, Authorization: workflowExecution.Authorization, @@ -3201,7 +3184,7 @@ func cleanupExecutions(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("[INFO] Api authentication failed in cleanup executions: %s", err) resp.WriteHeader(401) @@ -3221,7 +3204,7 @@ func cleanupExecutions(resp http.ResponseWriter, request *http.Request) { timestamp := int64(time.Now().AddDate(0, -2, 0).Unix()) log.Println(timestamp) q := datastore.NewQuery("workflowexecution").Filter("started_at <", timestamp) - var workflowExecutions []WorkflowExecution + var workflowExecutions []shuffle.WorkflowExecution _, err = dbclient.GetAll(ctx, q, &workflowExecutions) if err != nil { log.Printf("Error getting workflowexec (cleanup): %s", err) @@ -3234,13 +3217,13 @@ func cleanupExecutions(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": true}`)) } -func handleExecution(id string, workflow Workflow, request *http.Request) (WorkflowExecution, string, error) { +func handleExecution(id string, workflow shuffle.Workflow, request *http.Request) (shuffle.WorkflowExecution, string, error) { ctx := context.Background() if workflow.ID == "" || workflow.ID != id { - tmpworkflow, err := getWorkflow(ctx, id) + tmpworkflow, err := shuffle.GetWorkflow(ctx, id) if err != nil { log.Printf("Failed getting the workflow locally (execution cleanup): %s", err) - return WorkflowExecution{}, "Failed getting workflow", err + return shuffle.WorkflowExecution{}, "Failed getting workflow", err } workflow = *tmpworkflow @@ -3248,13 +3231,13 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if len(workflow.ExecutingOrg.Id) == 0 { log.Printf("[INFO] Stopped execution because there is no executing org for workflow %s", workflow.ID) - return WorkflowExecution{}, fmt.Sprintf("Workflow has no executing org defined"), errors.New("Workflow has no executing org defined") + return shuffle.WorkflowExecution{}, fmt.Sprintf("Workflow has no executing org defined"), errors.New("Workflow has no executing org defined") } if len(workflow.Actions) == 0 { - workflow.Actions = []Action{} + workflow.Actions = []shuffle.Action{} } else { - newactions := []Action{} + newactions := []shuffle.Action{} for _, action := range workflow.Actions { action.LargeImage = "" action.SmallImage = "" @@ -3266,12 +3249,12 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } if len(workflow.Branches) == 0 { - workflow.Branches = []Branch{} + workflow.Branches = []shuffle.Branch{} } if len(workflow.Triggers) == 0 { - workflow.Triggers = []Trigger{} + workflow.Triggers = []shuffle.Trigger{} } else { - newtriggers := []Trigger{} + newtriggers := []shuffle.Trigger{} for _, trigger := range workflow.Triggers { trigger.LargeImage = "" trigger.SmallImage = "" @@ -3288,21 +3271,21 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if !workflow.IsValid { log.Printf("[ERROR] Stopped execution as workflow %s is not valid.", workflow.ID) - return WorkflowExecution{}, fmt.Sprintf(`workflow %s is invalid`, workflow.ID), errors.New("Failed getting workflow") + return shuffle.WorkflowExecution{}, fmt.Sprintf(`workflow %s is invalid`, workflow.ID), errors.New("Failed getting workflow") } workflowBytes, err := json.Marshal(workflow) if err != nil { log.Printf("Failed workflow unmarshal in execution: %s", err) - return WorkflowExecution{}, "", err + return shuffle.WorkflowExecution{}, "", err } //log.Println(workflow) - var workflowExecution WorkflowExecution + var workflowExecution shuffle.WorkflowExecution err = json.Unmarshal(workflowBytes, &workflowExecution.Workflow) if err != nil { log.Printf("Failed execution unmarshaling: %s", err) - return WorkflowExecution{}, "Failed unmarshal during execution", err + return shuffle.WorkflowExecution{}, "Failed unmarshal during execution", err } makeNew := true @@ -3311,7 +3294,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf body, err := ioutil.ReadAll(request.Body) if err != nil { log.Printf("[ERROR] Failed request POST read: %s", err) - return WorkflowExecution{}, "Failed getting body", err + return shuffle.WorkflowExecution{}, "Failed getting body", err } // This one doesn't really matter. @@ -3353,11 +3336,11 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf log.Printf("Body: %s", string(body)) } - var execution ExecutionRequest + var execution shuffle.ExecutionRequest err = json.Unmarshal(body, &execution) if err != nil { log.Printf("[WARNING] Failed execution POST unmarshaling - continuing anyway: %s", err) - //return WorkflowExecution{}, "", err + //return shuffle.WorkflowExecution{}, "", err } if execution.Start == "" && len(body) > 0 { @@ -3388,12 +3371,12 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if !found { log.Printf("[ERROR] ACTION %s WAS NOT FOUND!", workflow.Start) - return WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", workflow.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", workflow.Start)) + return shuffle.WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", workflow.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", workflow.Start)) } } else if len(execution.Start) > 0 { log.Printf("[ERROR] START ACTION %s IS WRONG ID LENGTH %d!", execution.Start, len(execution.Start)) - return WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", execution.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", execution.Start)) + return shuffle.WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", execution.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", execution.Start)) } if len(execution.ExecutionId) == 36 { @@ -3415,18 +3398,18 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf log.Printf("Should update reference and return, no need for further execution!") // Get the reference execution - oldExecution, err := getWorkflowExecution(ctx, referenceId[0]) + oldExecution, err := shuffle.GetWorkflowExecution(ctx, referenceId[0]) if err != nil { log.Printf("Failed getting execution (execution) %s: %s", referenceId[0], err) - return WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err + return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err } if oldExecution.Workflow.ID != id { log.Println("Wrong workflowid!") - return WorkflowExecution{}, fmt.Sprintf("Bad ID %s", referenceId), errors.New("Bad ID") + return shuffle.WorkflowExecution{}, fmt.Sprintf("Bad ID %s", referenceId), errors.New("Bad ID") } - newResults := []ActionResult{} + newResults := []shuffle.ActionResult{} //log.Printf("%#v", oldExecution.Results) for _, result := range oldExecution.Results { log.Printf("%s - %s", result.Action.ID, start[0]) @@ -3453,20 +3436,20 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf err = setWorkflowExecution(ctx, *oldExecution, true) if err != nil { log.Printf("Error saving workflow execution actionresult setting: %s", err) - return WorkflowExecution{}, fmt.Sprintf("Failed setting workflowexecution actionresult in execution: %s", err), err + return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed setting workflowexecution actionresult in execution: %s", err), err } - return WorkflowExecution{}, "", nil + return shuffle.WorkflowExecution{}, "", nil } } if referenceok { log.Printf("Handling an old execution continuation!") // Will use the old name, but still continue with NEW ID - oldExecution, err := getWorkflowExecution(ctx, referenceId[0]) + oldExecution, err := shuffle.GetWorkflowExecution(ctx, referenceId[0]) if err != nil { log.Printf("Failed getting execution (execution) %s: %s", referenceId[0], err) - return WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err + return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err } workflowExecution = *oldExecution @@ -3492,7 +3475,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // FIXME - regex uuid, and check if already exists? if len(workflowExecution.ExecutionId) != 36 { log.Printf("Invalid uuid: %s", workflowExecution.ExecutionId) - return WorkflowExecution{}, "Invalid uuid", err + return shuffle.WorkflowExecution{}, "Invalid uuid", err } // FIXME - find owner of workflow @@ -3553,10 +3536,10 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf topic := "workflows" startFound := false // FIXME - remove this? - newActions := []Action{} - defaultResults := []ActionResult{} + newActions := []shuffle.Action{} + defaultResults := []shuffle.ActionResult{} - allAuths := []AppAuthenticationStorage{} + allAuths := []shuffle.AppAuthenticationStorage{} for _, action := range workflowExecution.Workflow.Actions { //action.LargeImage = "" if action.ID == workflowExecution.Start { @@ -3565,20 +3548,20 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf //log.Println(action.Environment) if action.Environment == "" { - return WorkflowExecution{}, fmt.Sprintf("Environment is not defined for %s", action.Name), errors.New("Environment not defined!") + return shuffle.WorkflowExecution{}, fmt.Sprintf("Environment is not defined for %s", action.Name), errors.New("Environment not defined!") } // FIXME: Authentication parameters if len(action.AuthenticationId) > 0 { if len(allAuths) == 0 { - allAuths, err = getAllWorkflowAppAuth(ctx, workflow.ExecutingOrg.Id) + allAuths, err = shuffle.GetAllWorkflowAppAuth(ctx, workflow.ExecutingOrg.Id) if err != nil { log.Printf("Api authentication failed in get all app auth: %s", err) - return WorkflowExecution{}, fmt.Sprintf("Api authentication failed in get all app auth: %s", err), err + return shuffle.WorkflowExecution{}, fmt.Sprintf("Api authentication failed in get all app auth: %s", err), err } } - curAuth := AppAuthenticationStorage{Id: ""} + curAuth := shuffle.AppAuthenticationStorage{Id: ""} for _, auth := range allAuths { if auth.Id == action.AuthenticationId { curAuth = auth @@ -3587,11 +3570,11 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } if len(curAuth.Id) == 0 { - return WorkflowExecution{}, fmt.Sprintf("Auth ID %s doesn't exist", action.AuthenticationId), errors.New(fmt.Sprintf("Auth ID %s doesn't exist", action.AuthenticationId)) + return shuffle.WorkflowExecution{}, fmt.Sprintf("Auth ID %s doesn't exist", action.AuthenticationId), errors.New(fmt.Sprintf("Auth ID %s doesn't exist", action.AuthenticationId)) } // Rebuild params with the right data. This is to prevent issues on the frontend - newParams := []WorkflowAppActionParameter{} + newParams := []shuffle.WorkflowAppActionParameter{} for _, param := range action.Parameters { for _, authparam := range curAuth.Fields { @@ -3620,7 +3603,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // as it's not a childnode of the startnode // This is a configuration item for the workflow itself. if len(workflowExecution.Results) > 0 { - defaultResults = []ActionResult{} + defaultResults = []shuffle.ActionResult{} for _, result := range workflowExecution.Results { if result.Status == "WAITING" { result.Status = "FINISHED" @@ -3644,7 +3627,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } //log.Printf("[WARNING] Set %s to SKIPPED as it's NOT a childnode of the startnode.", action.ID) - curaction := Action{ + curaction := shuffle.Action{ AppName: action.AppName, AppVersion: action.AppVersion, Label: action.Label, @@ -3653,7 +3636,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } //action //curaction.Parameters = [] - defaultResults = append(defaultResults, ActionResult{ + defaultResults = append(defaultResults, shuffle.ActionResult{ Action: curaction, ExecutionId: workflowExecution.ExecutionId, Authorization: workflowExecution.Authorization, @@ -3687,7 +3670,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if !found { //log.Printf("SHOULD SET TRIGGER %s TO BE SKIPPED", trigger.ID) - curaction := Action{ + curaction := shuffle.Action{ AppName: "shuffle-subflow", AppVersion: trigger.AppVersion, Label: trigger.Label, @@ -3695,7 +3678,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf ID: trigger.ID, } - defaultResults = append(defaultResults, ActionResult{ + defaultResults = append(defaultResults, shuffle.ActionResult{ Action: curaction, ExecutionId: workflowExecution.ExecutionId, Authorization: workflowExecution.Authorization, @@ -3713,7 +3696,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if !startFound { log.Printf("[ERROR] Startnode %s doesn't exist!!", workflowExecution.Start) - return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf(`Workflow start node "%s" doesn't exist. Exiting!`, workflowExecution.Start)) + return shuffle.WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf(`Workflow start node "%s" doesn't exist. Exiting!`, workflowExecution.Start)) } // Verification for execution environments @@ -3726,14 +3709,14 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf workflowExecution.ExecutionOrg = workflow.ExecutingOrg.Id } - var allEnvs []Environment + var allEnvs []shuffle.Environment if len(workflowExecution.ExecutionOrg) > 0 { //log.Printf("[INFO] Executing ORG: %s", workflowExecution.ExecutionOrg) - allEnvironments, err := getEnvironments(ctx, workflowExecution.ExecutionOrg) + allEnvironments, err := shuffle.GetEnvironments(ctx, workflowExecution.ExecutionOrg) if err != nil { log.Printf("Failed finding environments: %s", err) - return WorkflowExecution{}, fmt.Sprintf("Workflow environments not found for this org"), errors.New(fmt.Sprintf("Workflow environments not found for this org")) + return shuffle.WorkflowExecution{}, fmt.Sprintf("Workflow environments not found for this org"), errors.New(fmt.Sprintf("Workflow environments not found for this org")) } for _, curenv := range allEnvironments { @@ -3745,12 +3728,12 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } } else { log.Printf("[ERROR] No org identified for execution of %s. Returning", workflowExecution.Workflow.ID) - return WorkflowExecution{}, "No org identified for execution", errors.New("No org identified for execution") + return shuffle.WorkflowExecution{}, "No org identified for execution", errors.New("No org identified for execution") } if len(allEnvs) == 0 { log.Printf("[ERROR] No active environments found for org: %s", workflowExecution.ExecutionOrg) - return WorkflowExecution{}, "No active environments found", errors.New(fmt.Sprintf("No active env found for org %s", workflowExecution.ExecutionOrg)) + return shuffle.WorkflowExecution{}, "No active environments found", errors.New(fmt.Sprintf("No active env found for org %s", workflowExecution.ExecutionOrg)) } // Check if the actions are children of the startnode? @@ -3769,7 +3752,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf onpremExecution = true } else { log.Printf("[ERROR] No handler for environment type %s", env.Type) - return WorkflowExecution{}, "No active environments found", errors.New(fmt.Sprintf("No handler for environment type %s", env.Type)) + return shuffle.WorkflowExecution{}, "No active environments found", errors.New(fmt.Sprintf("No handler for environment type %s", env.Type)) } break } @@ -3777,7 +3760,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if !found { log.Printf("[ERROR] Couldn't find environment %s. Maybe it's inactive?", action.Environment) - return WorkflowExecution{}, "Couldn't find the environment", errors.New(fmt.Sprintf("Couldn't find env %s in org %s", action.Environment, workflowExecution.ExecutionOrg)) + return shuffle.WorkflowExecution{}, "Couldn't find the environment", errors.New(fmt.Sprintf("Couldn't find env %s in org %s", action.Environment, workflowExecution.ExecutionOrg)) } found = false @@ -3802,7 +3785,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf err = imageCheckBuilder(imageNames) if err != nil { log.Printf("[ERROR] Failed building the required images from %#v: %s", imageNames, err) - return WorkflowExecution{}, "Failed building missing Docker images", err + return shuffle.WorkflowExecution{}, "Failed building missing Docker images", err } //b, err := json.Marshal(workflowExecution) @@ -3812,17 +3795,17 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // //workflowExecution.ExecutionOrg.SyncFeatures = Org{} //} - workflowExecution.Workflow.ExecutingOrg = Org{ + workflowExecution.Workflow.ExecutingOrg = shuffle.Org{ Id: workflowExecution.Workflow.ExecutingOrg.Id, } - workflowExecution.Workflow.Org = []Org{ + workflowExecution.Workflow.Org = []shuffle.Org{ workflowExecution.Workflow.ExecutingOrg, } //Org []Org `json:"org,omitempty" datastore:"org"` err = setWorkflowExecution(ctx, workflowExecution, true) if err != nil { log.Printf("Error saving workflow execution for updates %s: %s", topic, err) - return WorkflowExecution{}, "Failed getting workflowexecution", err + return shuffle.WorkflowExecution{}, "Failed getting workflowexecution", err } // Adds queue for onprem execution @@ -3833,7 +3816,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf for _, environment := range environments { log.Printf("[INFO] Execution: %s should execute onprem with execution environment \"%s\". Workflow: %s", workflowExecution.ExecutionId, environment, workflowExecution.Workflow.ID) - executionRequest := ExecutionRequest{ + executionRequest := shuffle.ExecutionRequest{ ExecutionId: workflowExecution.ExecutionId, WorkflowId: workflowExecution.Workflow.ID, Authorization: workflowExecution.Authorization, @@ -3863,7 +3846,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if !featuresList.Workflows.Active || err != nil { log.Printf("Error: %s", err) log.Printf("[ERROR] Cloud not implemented yet. May need to work on app checking and such") - return WorkflowExecution{}, "Cloud not implemented yet", errors.New("Cloud not implemented yet") + return shuffle.WorkflowExecution{}, "Cloud not implemented yet", errors.New("Cloud not implemented yet") } // What it needs to know: @@ -3873,11 +3856,11 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf //cloudExecuteAction(workflowExecution.ExecutionId, workflowExecution.Workflow.Actions[0], workflowExecution.ExecutionOrg, workflowExecution.Workflow.ID) cloudExecuteAction(workflowExecution) - return WorkflowExecution{}, "Cloud not implemented yet (1)", errors.New("Cloud not implemented yet") + return shuffle.WorkflowExecution{}, "Cloud not implemented yet (1)", errors.New("Cloud not implemented yet") } else { // If it's here, it should be controlled by Worker. // If worker, should this backend be a proxy? I think so. - return WorkflowExecution{}, "Cloud not implemented yet (2)", errors.New("Cloud not implemented yet") + return shuffle.WorkflowExecution{}, "Cloud not implemented yet (2)", errors.New("Cloud not implemented yet") } } @@ -3890,21 +3873,21 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } // This updates stuff locally from remote executions -func cloudExecuteAction(execution WorkflowExecution) error { +func cloudExecuteAction(execution shuffle.WorkflowExecution) error { ctx := context.Background() - org, err := getOrg(ctx, execution.ExecutionOrg) + org, err := shuffle.GetOrg(ctx, execution.ExecutionOrg) if err != nil { return err } type ExecutionStruct struct { - ExecutionId string `json:"execution_id" datastore:"execution_id"` - Action Action `json:"action" datastore:"action"` - Authorization string `json:"authorization" datastore:"authorization"` - Results []ActionResult `json:"results" datastore:"results,noindex"` - ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` - WorkflowId string `json:"workflow_id" datastore:"workflow_id"` - ExecutionSource string `json:"execution_source" datastore:"execution_source"` + ExecutionId string `json:"execution_id" datastore:"execution_id"` + Action shuffle.Action `json:"action" datastore:"action"` + Authorization string `json:"authorization" datastore:"authorization"` + Results []shuffle.ActionResult `json:"results" datastore:"results,noindex"` + ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` + WorkflowId string `json:"workflow_id" datastore:"workflow_id"` + ExecutionSource string `json:"execution_source" datastore:"execution_source"` } data := ExecutionStruct{ @@ -3966,7 +3949,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("[INFO] Api authentication failed in execute workflow: %s", err) resp.WriteHeader(401) @@ -3995,7 +3978,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { //memcacheName := fmt.Sprintf("%s_%s", user.Username, fileId) ctx := context.Background() - workflow, err := getWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Failed getting the workflow locally (execute workflow): %s", err) resp.WriteHeader(401) @@ -4014,7 +3997,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] Starting execution of %s!", fileId) - user.ActiveOrg.Users = []User{} + user.ActiveOrg.Users = []shuffle.User{} workflow.ExecutingOrg = user.ActiveOrg workflowExecution, executionResp, err := handleExecution(fileId, *workflow, request) @@ -4034,7 +4017,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in schedule workflow: %s", err) resp.WriteHeader(401) @@ -4070,7 +4053,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflow, err := getWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("[WARNING] Failed getting the workflow locally (stop schedule): %s", err) resp.WriteHeader(401) @@ -4087,7 +4070,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { return } - schedule, err := getSchedule(ctx, scheduleId) + schedule, err := shuffle.GetSchedule(ctx, scheduleId) if err != nil { log.Printf("[WARNING] Failed finding schedule %s", scheduleId) resp.WriteHeader(401) @@ -4100,7 +4083,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { if schedule.Environment == "cloud" { log.Printf("[INFO] Should STOP a cloud schedule for workflow %s with schedule ID %s", fileId, scheduleId) // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c - org, err := getOrg(ctx, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed finding org %s: %s", org.Id, err) return @@ -4163,7 +4146,7 @@ func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in schedule workflow: %s", err) resp.WriteHeader(401) @@ -4199,7 +4182,7 @@ func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflow, err := getWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Failed getting the workflow locally (stop schedule GCP): %s", err) resp.WriteHeader(401) @@ -4217,13 +4200,13 @@ func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) { } if len(workflow.Actions) == 0 { - workflow.Actions = []Action{} + workflow.Actions = []shuffle.Action{} } if len(workflow.Branches) == 0 { - workflow.Branches = []Branch{} + workflow.Branches = []shuffle.Branch{} } if len(workflow.Triggers) == 0 { - workflow.Triggers = []Trigger{} + workflow.Triggers = []shuffle.Trigger{} } if len(workflow.Errors) == 0 { workflow.Errors = []string{} @@ -4292,7 +4275,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in schedule workflow: %s", err) resp.WriteHeader(401) @@ -4320,7 +4303,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflow, err := getWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Failed getting the workflow locally (schedule workflow): %s", err) resp.WriteHeader(401) @@ -4338,13 +4321,13 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { } if len(workflow.Actions) == 0 { - workflow.Actions = []Action{} + workflow.Actions = []shuffle.Action{} } if len(workflow.Branches) == 0 { - workflow.Branches = []Branch{} + workflow.Branches = []shuffle.Branch{} } if len(workflow.Triggers) == 0 { - workflow.Triggers = []Trigger{} + workflow.Triggers = []shuffle.Trigger{} } if len(workflow.Errors) == 0 { workflow.Errors = []string{} @@ -4358,7 +4341,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { return } - var schedule Schedule + var schedule shuffle.Schedule err = json.Unmarshal(body, &schedule) if err != nil { log.Printf("Failed schedule POST unmarshaling: %s", err) @@ -4422,7 +4405,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { if schedule.Environment == "cloud" { log.Printf("[INFO] Should START a cloud schedule for workflow %s with schedule ID %s", workflow.ID, schedule.Id) // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c - org, err := getOrg(ctx, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed finding org %s: %s", org.Id, err) return @@ -4523,7 +4506,7 @@ func getSpecificWorkflow(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in getting specific workflow: %s", err) resp.WriteHeader(401) @@ -4569,7 +4552,7 @@ func getSpecificWorkflow(resp http.ResponseWriter, request *http.Request) { // return //} - workflow, err := getWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Workflow %s doesn't exist.", fileId) resp.WriteHeader(401) @@ -4588,13 +4571,13 @@ func getSpecificWorkflow(resp http.ResponseWriter, request *http.Request) { } if len(workflow.Actions) == 0 { - workflow.Actions = []Action{} + workflow.Actions = []shuffle.Action{} } if len(workflow.Branches) == 0 { - workflow.Branches = []Branch{} + workflow.Branches = []shuffle.Branch{} } if len(workflow.Triggers) == 0 { - workflow.Triggers = []Trigger{} + workflow.Triggers = []shuffle.Trigger{} } if len(workflow.Errors) == 0 { workflow.Errors = []string{} @@ -4643,7 +4626,7 @@ func getSpecificWorkflow(resp http.ResponseWriter, request *http.Request) { resp.Write(body) } -func setWorkflowExecution(ctx context.Context, workflowExecution WorkflowExecution, dbSave bool) error { +func setWorkflowExecution(ctx context.Context, workflowExecution shuffle.WorkflowExecution, dbSave bool) error { //log.Printf("\n\n\nRESULT: %s\n\n\n", workflowExecution.Status) if len(workflowExecution.ExecutionId) == 0 { log.Printf("Workflowexeciton executionId can't be empty.") @@ -4667,11 +4650,11 @@ func setWorkflowExecution(ctx context.Context, workflowExecution WorkflowExecuti return nil } -func getWorkflowExecution(ctx context.Context, id string) (*WorkflowExecution, error) { - workflowExecution := &WorkflowExecution{} +func getWorkflowExecution(ctx context.Context, id string) (*shuffle.WorkflowExecution, error) { + workflowExecution := &shuffle.WorkflowExecution{} cacheKey := fmt.Sprintf("workflowexecution-%s", id) if value, found := requestCache.Get(cacheKey); found { - parsedValue := value.(*WorkflowExecution) + parsedValue := value.(*shuffle.WorkflowExecution) //log.Printf("Found execution for id %s with %d results", parsedValue.ExecutionId, len(parsedValue.Results)) return parsedValue, nil @@ -4688,58 +4671,46 @@ func getWorkflowExecution(ctx context.Context, id string) (*WorkflowExecution, e key := datastore.NameKey("workflowexecution", strings.ToLower(id), nil) if err := dbclient.Get(ctx, key, workflowExecution); err != nil { - return &WorkflowExecution{}, err + return &shuffle.WorkflowExecution{}, err } return workflowExecution, nil } -func getApp(ctx context.Context, id string) (*WorkflowApp, error) { - key := datastore.NameKey("workflowapp", strings.ToLower(id), nil) - workflowApp := &WorkflowApp{} - if err := dbclient.Get(ctx, key, workflowApp); err != nil { - return &WorkflowApp{}, err +//func shuffle.GetApp(ctx context.Context, id string) (*WorkflowApp, error) { +// key := datastore.NameKey("workflowapp", strings.ToLower(id), nil) +// workflowApp := &WorkflowApp{} +// if err := dbclient.Get(ctx, key, workflowApp); err != nil { +// return &WorkflowApp{}, err +// +// } +// +// return workflowApp, nil +//} +// +//func shuffle.GetWorkflow(ctx context.Context, id string) (*shuffle.Workflow, error) { +// key := datastore.NameKey("workflow", strings.ToLower(id), nil) +// workflow := &Workflow{} +// if err := dbclient.Get(ctx, key, workflow); err != nil { +// return &Workflow{}, err +// } +// +// return workflow, nil +//} +// +//func shuffle.GetEnvironments(ctx context.Context, orgId string) ([]Environment, error) { +// var environments []Environment +// q := datastore.NewQuery("Environments").Filter("org_id =", orgId) +// +// _, err := dbclient.GetAll(ctx, q, &environments) +// if err != nil { +// return []Environment{}, err +// } +// +// return environments, nil +//} - } - - return workflowApp, nil -} - -func getWorkflow(ctx context.Context, id string) (*Workflow, error) { - key := datastore.NameKey("workflow", strings.ToLower(id), nil) - workflow := &Workflow{} - if err := dbclient.Get(ctx, key, workflow); err != nil { - return &Workflow{}, err - } - - return workflow, nil -} - -func getEnvironments(ctx context.Context, orgId string) ([]Environment, error) { - var environments []Environment - q := datastore.NewQuery("Environments").Filter("org_id =", orgId) - - _, err := dbclient.GetAll(ctx, q, &environments) - if err != nil { - return []Environment{}, err - } - - return environments, nil -} - -func getAllWorkflows(ctx context.Context, orgId string) ([]Workflow, error) { - var allworkflows []Workflow - q := datastore.NewQuery("workflow").Filter("org_id = ", orgId) - - _, err := dbclient.GetAll(ctx, q, &allworkflows) - if err != nil { - return []Workflow{}, err - } - - return allworkflows, nil -} - -func setExampleresult(ctx context.Context, result AppExecutionExample) error { +func setExampleresult(ctx context.Context, result shuffle.AppExecutionExample) error { // FIXME: Reintroduce this for stats //key := datastore.NameKey("example_result", result.ExampleId, nil) @@ -4754,7 +4725,7 @@ func setExampleresult(ctx context.Context, result AppExecutionExample) error { // Hmm, so I guess this should use uuid :( // Consistency PLX -func setWorkflow(ctx context.Context, workflow Workflow, id string, optionalEditedSecondsOffset ...int) error { +func setWorkflow(ctx context.Context, workflow shuffle.Workflow, id string, optionalEditedSecondsOffset ...int) error { workflow.Edited = int64(time.Now().Unix()) if len(optionalEditedSecondsOffset) > 0 { workflow.Edited += int64(optionalEditedSecondsOffset[0]) @@ -4777,7 +4748,7 @@ func deleteAppAuthentication(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in edit workflow: %s", userErr) resp.WriteHeader(401) @@ -4831,7 +4802,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in edit workflow: %s", userErr) resp.WriteHeader(401) @@ -4854,7 +4825,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() log.Printf("ID: %s", fileId) - app, err := getApp(ctx, fileId) + app, err := shuffle.GetApp(ctx, fileId) if err != nil { log.Printf("Error getting app (delete) %s: %s", fileId, err) resp.WriteHeader(401) @@ -4879,7 +4850,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { // FIXME: Make workflows track themself INSIDE apps, or with a reference q := datastore.NewQuery("workflow").Filter("org_id = ", user.ActiveOrg.Id).Limit(30) - var workflows []Workflow + var workflows []shuffle.Workflow _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { log.Printf("Failed getting related workflows for the app: %s", err) @@ -4893,7 +4864,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { for _, workflow := range workflows { found := false - newActions := []Action{} + newActions := []shuffle.Action{} for _, action := range workflow.Actions { if action.AppName == app.Name && action.AppVersion == app.AppVersion { found = true @@ -4944,7 +4915,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { // Not really deleting it, just removing from user cache if private { log.Printf("[INFO] Deleting private app") - var privateApps []WorkflowApp + var privateApps []shuffle.WorkflowApp for _, item := range user.PrivateApps { if item.ID == fileId { continue @@ -4954,7 +4925,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { } user.PrivateApps = privateApps - err = setUser(ctx, &user) + err = shuffle.SetUser(ctx, &user) if err != nil { log.Printf("[ERROR] Failed removing %s app for user %s: %s", app.Name, user.Username, err) resp.WriteHeader(401) @@ -5006,7 +4977,7 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { fileId = location[4] } - app, err := getApp(ctx, fileId) + app, err := shuffle.GetApp(ctx, fileId) if err != nil { log.Printf("[WARNING] Error getting app %s (app config): %s", fileId, err) resp.WriteHeader(401) @@ -5051,7 +5022,7 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("[WARNING] Api authentication failed in get app: %s", userErr) resp.WriteHeader(401) @@ -5099,7 +5070,7 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in get all apps: %s", userErr) resp.WriteHeader(401) @@ -5155,7 +5126,7 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - auth, err := getWorkflowAppAuthDatastore(ctx, fileId) + auth, err := shuffle.GetWorkflowAppAuthDatastore(ctx, fileId) if err != nil { log.Printf("Authget error: %s", err) resp.WriteHeader(401) @@ -5174,7 +5145,7 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { q := datastore.NewQuery("workflow").Filter("org_id =", user.ActiveOrg.Id) q = q.Order("-edited").Limit(35) - var workflows []Workflow + var workflows []shuffle.Workflow _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { log.Printf("Getall error in auth update: %s", err) @@ -5186,11 +5157,11 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { // FIXME: Add function to remove auth from other auth's actionCnt := 0 workflowCnt := 0 - authenticationUsage := []AuthenticationUsage{} + authenticationUsage := []shuffle.AuthenticationUsage{} for _, workflow := range workflows { - newActions := []Action{} + newActions := []shuffle.Action{} edited := false - usage := AuthenticationUsage{ + usage := shuffle.AuthenticationUsage{ WorkflowId: workflow.ID, Nodes: []string{}, } @@ -5229,7 +5200,7 @@ func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { auth.Usage = authenticationUsage auth.Defined = true - err = setWorkflowAppAuthDatastore(ctx, *auth, auth.Id) + err = shuffle.SetWorkflowAppAuthDatastore(ctx, *auth, auth.Id) if err != nil { log.Printf("Failed setting appauth: %s", err) resp.WriteHeader(401) @@ -5254,7 +5225,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in get all apps: %s", userErr) resp.WriteHeader(401) @@ -5270,7 +5241,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { return } - var appAuth AppAuthenticationStorage + var appAuth shuffle.AppAuthenticationStorage err = json.Unmarshal(body, &appAuth) if err != nil { log.Printf("Failed unmarshaling (appauth): %s", err) @@ -5283,7 +5254,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { if len(appAuth.Id) == 0 { appAuth.Id = uuid.NewV4().String() } else { - auth, err := getWorkflowAppAuthDatastore(ctx, appAuth.Id) + auth, err := shuffle.GetWorkflowAppAuthDatastore(ctx, appAuth.Id) if err == nil { // OrgId string `json:"org_id" datastore:"org_id"` if auth.OrgId != user.ActiveOrg.Id { @@ -5331,10 +5302,10 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { } // FIXME: Doens't validate Org - app, err := getApp(ctx, appAuth.App.ID) + app, err := shuffle.GetApp(ctx, appAuth.App.ID) if err != nil { log.Printf("[WARNING] Failed finding app %s while setting auth. Finding it by looping apps.", appAuth.App.ID) - workflowapps, err := getAllWorkflowApps(ctx, 500) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) if err != nil { resp.WriteHeader(409) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) @@ -5379,7 +5350,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { //appAuth.LargeImage = "" appAuth.OrgId = user.ActiveOrg.Id appAuth.Defined = true - err = setWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) + err = shuffle.SetWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) if err != nil { log.Printf("Failed setting up app auth %s: %s", appAuth.Id, err) resp.WriteHeader(409) @@ -5397,7 +5368,7 @@ func getAppAuthentication(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in get all apps: %s", userErr) resp.WriteHeader(401) @@ -5413,7 +5384,7 @@ func getAppAuthentication(resp http.ResponseWriter, request *http.Request) { // return //} ctx := context.Background() - allAuths, err := getAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) + allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Api authentication failed in get all app auth: %s", err) resp.WriteHeader(401) @@ -5428,7 +5399,7 @@ func getAppAuthentication(resp http.ResponseWriter, request *http.Request) { } // Cleanup for frontend usage. User shouldn't be able to get the data. - newAuth := []AppAuthenticationStorage{} + newAuth := []shuffle.AppAuthenticationStorage{} for _, auth := range allAuths { newAuthField := auth for index, _ := range auth.Fields { @@ -5509,7 +5480,7 @@ func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { return } - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in get all apps: %s", userErr) resp.WriteHeader(401) @@ -5530,7 +5501,7 @@ func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - app, err := getApp(ctx, fileId) + app, err := shuffle.GetApp(ctx, fileId) if err != nil { log.Printf("Error getting app (update app): %s", fileId) resp.WriteHeader(401) @@ -5575,7 +5546,7 @@ func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { app.SharingConfig = tmpfields.SharingConfig } - err = setWorkflowAppDatastore(ctx, *app, app.ID) + err = shuffle.SetWorkflowAppDatastore(ctx, *app, app.ID) if err != nil { log.Printf("Failed patching workflowapp: %s", err) resp.WriteHeader(401) @@ -5606,7 +5577,7 @@ func getWorkflowApps(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() // Just need to be logged in // FIXME - need to be logged in? - user, userErr := handleApiAuthentication(resp, request) + user, userErr := shuffle.HandleApiAuthentication(resp, request) if userErr != nil { log.Printf("Continuing with apps even without auth") //log.Printf("Api authentication failed in get all apps: %s", userErr) @@ -5643,7 +5614,7 @@ func getWorkflowApps(resp http.ResponseWriter, request *http.Request) { // return //} - workflowapps, err := getAllWorkflowApps(ctx, 500) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) if err != nil { log.Printf("Failed getting apps (getworkflowapps): %s", err) resp.WriteHeader(401) @@ -5756,7 +5727,7 @@ func getWorkflowApps(resp http.ResponseWriter, request *http.Request) { // Bad check for workflowapps :) // FIXME - use tags and struct reflection -func checkWorkflowApp(workflowApp WorkflowApp) error { +func checkWorkflowApp(workflowApp shuffle.WorkflowApp) error { // Validate fields if workflowApp.Name == "" { return errors.New("App field name doesn't exist") @@ -5804,7 +5775,7 @@ func getSpecificApps(resp http.ResponseWriter, request *http.Request) { // Just need to be logged in // FIXME - should have some permissions? - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new app: %s", err) resp.WriteHeader(401) @@ -5836,7 +5807,7 @@ func getSpecificApps(resp http.ResponseWriter, request *http.Request) { // FIXME - continue the search here with github repos etc. // Caching might be smart :D ctx := context.Background() - workflowapps, err := getAllWorkflowApps(ctx, 500) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) if err != nil { log.Printf("Error: Failed getting workflowapps: %s", err) resp.WriteHeader(401) @@ -5844,7 +5815,7 @@ func getSpecificApps(resp http.ResponseWriter, request *http.Request) { return } - returnValues := []WorkflowApp{} + returnValues := []shuffle.WorkflowApp{} search := strings.ToLower(tmpBody.Search) for _, app := range workflowapps { if !app.Activated && app.Generated { @@ -5879,7 +5850,7 @@ func validateAppInput(resp http.ResponseWriter, request *http.Request) { // Just need to be logged in // FIXME - should have some permissions? - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new app: %s", err) resp.WriteHeader(401) @@ -6138,7 +6109,7 @@ func loadSpecificWorkflows(resp http.ResponseWriter, request *http.Request) { // Just need to be logged in // FIXME - should have some permissions? - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in load apps: %s", err) resp.WriteHeader(401) @@ -6205,7 +6176,7 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { // Just need to be logged in // FIXME - should have some permissions? - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in app hotload: %s", err) resp.WriteHeader(401) @@ -6252,7 +6223,7 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) { // Just need to be logged in // FIXME - should have some permissions? - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in load specific apps: %s", err) resp.WriteHeader(401) @@ -6378,7 +6349,7 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) { func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string) error { ctx := context.Background() - workflowapps, err := getAllWorkflowApps(ctx, 500) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) appCounter := 0 if err != nil { log.Printf("Failed to get existing generated apps") @@ -6465,7 +6436,7 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, } //log.Printf("Should generate yaml") - swagger, api, _, err := generateYaml(swagger, parsedOpenApi.ID) + swagger, api, _, err := shuffle.GenerateYaml(swagger, parsedOpenApi.ID) if err != nil { log.Printf("Failed building and generating yaml in loop (%s): %s", filename, err) continue @@ -6490,7 +6461,7 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, } if !found { - err = setWorkflowAppDatastore(ctx, api, api.ID) + err = shuffle.SetWorkflowAppDatastore(ctx, api, api.ID) if err != nil { log.Printf("Failed setting workflowapp in loop: %s", err) continue @@ -6582,7 +6553,7 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra continue } - var workflow Workflow + var workflow shuffle.Workflow err = json.Unmarshal(readFile, &workflow) if err != nil { continue @@ -6595,11 +6566,11 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra workflow.ID = uuid.NewV4().String() workflow.OrgId = orgId - workflow.ExecutingOrg = Org{ + workflow.ExecutingOrg = shuffle.Org{ Id: orgId, } - workflow.Org = append(workflow.Org, Org{ + workflow.Org = append(workflow.Org, shuffle.Org{ Id: orgId, }) workflow.IsValid = false @@ -6646,7 +6617,7 @@ type buildLaterStruct struct { func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string, forceUpdate bool) ([]buildLaterStruct, []buildLaterStruct, error) { var err error - allapps := []WorkflowApp{} + allapps := []shuffle.WorkflowApp{} // These are slow apps to build with some funky mechanisms reservedNames := []string{ @@ -6757,7 +6728,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin combined = append(combined, dockerfileData...) md5 := md5sum(combined) - var workflowapp WorkflowApp + var workflowapp shuffle.WorkflowApp err = gyaml.Unmarshal(appfileData, &workflowapp) if err != nil { log.Printf("Failed unmarshaling workflowapp %s: %s", fullPath, err) @@ -6772,7 +6743,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin } if len(allapps) == 0 { - allapps, err = getAllWorkflowApps(ctx, 500) + allapps, err = shuffle.GetAllWorkflowApps(ctx, 500) if err != nil { log.Printf("Failed getting apps to verify: %s", err) continue @@ -6816,7 +6787,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin // 2. Check if they're present in the action // 3. Add them IF they DONT exist // 4. Fix python code with reflection (FIXME) - appendParams := []WorkflowAppActionParameter{} + appendParams := []shuffle.WorkflowAppActionParameter{} for _, fieldname := range workflowapp.Authentication.Parameters { found := false for index, param := range action.Parameters { @@ -6830,7 +6801,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin } if !found { - appendParams = append(appendParams, WorkflowAppActionParameter{ + appendParams = append(appendParams, shuffle.WorkflowAppActionParameter{ Name: fieldname.Name, Description: fieldname.Description, Example: fieldname.Example, @@ -6872,7 +6843,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin workflowapp.Downloaded = true workflowapp.Hash = md5 - err = setWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) + err = shuffle.SetWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) if err != nil { log.Printf("Failed setting workflowapp: %s", err) continue @@ -6972,7 +6943,7 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { // Just need to be logged in // FIXME - should have some permissions? - _, err := handleApiAuthentication(resp, request) + _, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new app: %s", err) resp.WriteHeader(401) @@ -6988,7 +6959,7 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { return } - var workflowapp WorkflowApp + var workflowapp shuffle.WorkflowApp err = json.Unmarshal(body, &workflowapp) if err != nil { log.Printf("Failed unmarshaling: %s", err) @@ -6998,7 +6969,7 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - allapps, err := getAllWorkflowApps(ctx, 500) + allapps, err := shuffle.GetAllWorkflowApps(ctx, 500) if err != nil { log.Printf("Failed getting apps to verify: %s", err) resp.WriteHeader(401) @@ -7039,7 +7010,7 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { workflowapp.Generated = false workflowapp.Activated = true - err = setWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) + err = shuffle.SetWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) if err != nil { log.Printf("Failed setting workflowapp: %s", err) resp.WriteHeader(401) @@ -7065,7 +7036,7 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in getting specific workflow: %s", err) resp.WriteHeader(401) @@ -7093,7 +7064,7 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - workflow, err := getWorkflow(ctx, fileId) + workflow, err := shuffle.GetWorkflow(ctx, fileId) if err != nil { log.Printf("Failed getting the workflow %s locally (get executions): %s", fileId, err) resp.WriteHeader(401) @@ -7112,7 +7083,7 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { // Query for the specifci workflowId maxAmount := 30 q := datastore.NewQuery("workflowexecution").Filter("workflow_id =", fileId).Order("-started_at").Limit(maxAmount) - var workflowExecutions []WorkflowExecution + var workflowExecutions []shuffle.WorkflowExecution _, err = dbclient.GetAll(ctx, q, &workflowExecutions) if err != nil { @@ -7133,7 +7104,7 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { it := dbclient.Run(ctx, q) //_, err = it.Next(&app) for { - var workflowExecution WorkflowExecution + var workflowExecution shuffle.WorkflowExecution _, err := it.Next(&workflowExecution) if err != nil { break @@ -7227,169 +7198,169 @@ func getAllSchedules(ctx context.Context, orgId string) ([]ScheduleOld, error) { } //FIXME: Add cursor -func getAllWorkflowApps(ctx context.Context, maxLen int) ([]WorkflowApp, error) { - var apps []WorkflowApp - query := datastore.NewQuery("workflowapp").Order("-edited").Limit(10) - //query := datastore.NewQuery("workflowapp").Order("-edited").Limit(40) +//func shuffle.GetAllWorkflowApps(ctx context.Context, maxLen int) ([]shuffle.WorkflowApp, error) { +// var apps []WorkflowApp +// query := datastore.NewQuery("workflowapp").Order("-edited").Limit(10) +// //query := datastore.NewQuery("workflowapp").Order("-edited").Limit(40) +// +// cacheKey := fmt.Sprintf("workflowapps-sorted-%d", maxLen) +// if value, found := requestCache.Get(cacheKey); found { +// parsedValue := value.(*[]WorkflowApp) +// log.Printf("[INFO] Returning %d apps from cache", len(*parsedValue)) +// return *parsedValue, nil +// } +// +// cursorStr := "" +// +// // NOT BEING UPDATED +// // FIXME: Update the app with the correct actions. HOW DOES THIS WORK?? +// // Seems like only actions are wrong. Could get the app individually. +// // Guessing it's a memory issue. +// //Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions,noindex"` +// //errors.New(nil) +// var err error +// for { +// it := dbclient.Run(ctx, query) +// //_, err = it.Next(&app) +// for { +// var app WorkflowApp +// _, err := it.Next(&app) +// if err != nil { +// break +// } +// +// if app.Name == "Shuffle Subflow" { +// continue +// } +// +// found := false +// //log.Printf("ACTIONS: %d - %s", len(app.Actions), app.Name) +// for _, innerapp := range apps { +// if innerapp.Name == app.Name { +// found = true +// break +// } +// } +// +// if !found { +// apps = append(apps, app) +// } +// } +// +// if err != iterator.Done { +// //log.Printf("[INFO] Failed fetching results: %v", err) +// //break +// } +// +// // Get the cursor for the next page of results. +// nextCursor, err := it.Cursor() +// if err != nil { +// log.Printf("Cursorerror: %s", err) +// break +// } else { +// //log.Printf("NEXTCURSOR: %s", nextCursor) +// nextStr := fmt.Sprintf("%s", nextCursor) +// if cursorStr == nextStr { +// break +// } +// +// cursorStr = nextStr +// query = query.Start(nextCursor) +// //cursorStr = nextCursor +// //break +// } +// +// if len(apps) > maxLen { +// break +// } +// } +// +// if len(apps) > 20 { +// log.Printf("[INFO] Setting %d apps in cache", len(apps)) +// requestCache.Set(cacheKey, &apps, cache.DefaultExpiration) +// } +// +// //var allworkflowapps []WorkflowApp +// //_, err := dbclient.GetAll(ctx, query, &allworkflowapps) +// //if err != nil { +// // if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { +// // //datastore.NewQuery("workflowapp").Limit(30).Order("-edited") +// // query = datastore.NewQuery("workflowapp").Order("-edited").Limit(25) +// // //q := q.Limit(25) +// // _, err := dbclient.GetAll(ctx, query, &allworkflowapps) +// // if err != nil { +// // return []WorkflowApp{}, err +// // } +// // } else { +// // return []WorkflowApp{}, err +// // } +// //} +// +// return apps, nil +//} - cacheKey := fmt.Sprintf("workflowapps-sorted-%d", maxLen) - if value, found := requestCache.Get(cacheKey); found { - parsedValue := value.(*[]WorkflowApp) - log.Printf("[INFO] Returning %d apps from cache", len(*parsedValue)) - return *parsedValue, nil - } - - cursorStr := "" - - // NOT BEING UPDATED - // FIXME: Update the app with the correct actions. HOW DOES THIS WORK?? - // Seems like only actions are wrong. Could get the app individually. - // Guessing it's a memory issue. - //Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions,noindex"` - //errors.New(nil) - var err error - for { - it := dbclient.Run(ctx, query) - //_, err = it.Next(&app) - for { - var app WorkflowApp - _, err := it.Next(&app) - if err != nil { - break - } - - if app.Name == "Shuffle Subflow" { - continue - } - - found := false - //log.Printf("ACTIONS: %d - %s", len(app.Actions), app.Name) - for _, innerapp := range apps { - if innerapp.Name == app.Name { - found = true - break - } - } - - if !found { - apps = append(apps, app) - } - } - - if err != iterator.Done { - //log.Printf("[INFO] Failed fetching results: %v", err) - //break - } - - // Get the cursor for the next page of results. - nextCursor, err := it.Cursor() - if err != nil { - log.Printf("Cursorerror: %s", err) - break - } else { - //log.Printf("NEXTCURSOR: %s", nextCursor) - nextStr := fmt.Sprintf("%s", nextCursor) - if cursorStr == nextStr { - break - } - - cursorStr = nextStr - query = query.Start(nextCursor) - //cursorStr = nextCursor - //break - } - - if len(apps) > maxLen { - break - } - } - - if len(apps) > 20 { - log.Printf("[INFO] Setting %d apps in cache", len(apps)) - requestCache.Set(cacheKey, &apps, cache.DefaultExpiration) - } - - //var allworkflowapps []WorkflowApp - //_, err := dbclient.GetAll(ctx, query, &allworkflowapps) - //if err != nil { - // if strings.Contains(fmt.Sprintf("%s", err), "ResourceExhausted") { - // //datastore.NewQuery("workflowapp").Limit(30).Order("-edited") - // query = datastore.NewQuery("workflowapp").Order("-edited").Limit(25) - // //q := q.Limit(25) - // _, err := dbclient.GetAll(ctx, query, &allworkflowapps) - // if err != nil { - // return []WorkflowApp{}, err - // } - // } else { - // return []WorkflowApp{}, err - // } - //} - - return apps, nil -} - -func getAllWorkflowAppAuth(ctx context.Context, OrgId string) ([]AppAuthenticationStorage, error) { - var allworkflowapps []AppAuthenticationStorage - q := datastore.NewQuery("workflowappauth").Filter("org_id = ", OrgId) - - _, err := dbclient.GetAll(ctx, q, &allworkflowapps) - if err != nil { - return []AppAuthenticationStorage{}, err - } - - return allworkflowapps, nil -} - -func getWorkflowAppAuthDatastore(ctx context.Context, id string) (*AppAuthenticationStorage, error) { - - key := datastore.NameKey("workflowappauth", id, nil) - appAuth := &AppAuthenticationStorage{} - // New struct, to not add body, author etc - if err := dbclient.Get(ctx, key, appAuth); err != nil { - return &AppAuthenticationStorage{}, err - } - - return appAuth, nil -} - -func setWorkflowAppAuthDatastore(ctx context.Context, workflowappauth AppAuthenticationStorage, id string) error { - timeNow := int64(time.Now().Unix()) - if workflowappauth.Created == 0 { - workflowappauth.Created = timeNow - } - - workflowappauth.Edited = timeNow - - key := datastore.NameKey("workflowappauth", id, nil) - - // New struct, to not add body, author etc - if _, err := dbclient.Put(ctx, key, &workflowappauth); err != nil { - log.Printf("Error adding workflow app auth: %s", err) - return err - } - - return nil -} - -// Hmm, so I guess this should use uuid :( -// Consistency PLX -func setWorkflowAppDatastore(ctx context.Context, workflowapp WorkflowApp, id string) error { - timeNow := int64(time.Now().Unix()) - if workflowapp.Created == 0 { - workflowapp.Created = timeNow - } - - workflowapp.Edited = timeNow - key := datastore.NameKey("workflowapp", id, nil) - - // New struct, to not add body, author etc - if _, err := dbclient.Put(ctx, key, &workflowapp); err != nil { - log.Printf("Error adding workflow app: %s", err) - return err - } - - return nil -} +//func shuffle.GetAllWorkflowAppAuth(ctx context.Context, OrgId string) ([]shuffle.AppAuthenticationStorage, error) { +// var allworkflowapps []AppAuthenticationStorage +// q := datastore.NewQuery("workflowappauth").Filter("org_id = ", OrgId) +// +// _, err := dbclient.GetAll(ctx, q, &allworkflowapps) +// if err != nil { +// return []AppAuthenticationStorage{}, err +// } +// +// return allworkflowapps, nil +//} +// +//func getWorkflowAppAuthDatastore(ctx context.Context, id string) (*AppAuthenticationStorage, error) { +// +// key := datastore.NameKey("workflowappauth", id, nil) +// appAuth := &AppAuthenticationStorage{} +// // New struct, to not add body, author etc +// if err := dbclient.Get(ctx, key, appAuth); err != nil { +// return &AppAuthenticationStorage{}, err +// } +// +// return appAuth, nil +//} +// +//func shuffle.SetWorkflowAppAuthDatastore(ctx context.Context, workflowappauth AppAuthenticationStorage, id string) error { +// timeNow := int64(time.Now().Unix()) +// if workflowappauth.Created == 0 { +// workflowappauth.Created = timeNow +// } +// +// workflowappauth.Edited = timeNow +// +// key := datastore.NameKey("workflowappauth", id, nil) +// +// // New struct, to not add body, author etc +// if _, err := dbclient.Put(ctx, key, &workflowappauth); err != nil { +// log.Printf("Error adding workflow app auth: %s", err) +// return err +// } +// +// return nil +//} +// +//// Hmm, so I guess this should use uuid :( +//// Consistency PLX +//func SetWorkflowAppDatastore(ctx context.Context, workflowapp WorkflowApp, id string) error { +// timeNow := int64(time.Now().Unix()) +// if workflowapp.Created == 0 { +// workflowapp.Created = timeNow +// } +// +// workflowapp.Edited = timeNow +// key := datastore.NameKey("workflowapp", id, nil) +// +// // New struct, to not add body, author etc +// if _, err := dbclient.Put(ctx, key, &workflowapp); err != nil { +// log.Printf("Error adding workflow app: %s", err) +// return err +// } +// +// return nil +//} // Starts a new webhook func handleStopHook(resp http.ResponseWriter, request *http.Request) { @@ -7398,7 +7369,7 @@ func handleStopHook(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -7481,7 +7452,7 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -7543,7 +7514,7 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { log.Printf("Hook: %#v", hook) if hook.Environment == "cloud" { log.Printf("[INFO] Should STOP cloud webhook https://shuffler.io/api/v1/hooks/webhook_%s", hook.Id) - org, err := getOrg(ctx, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed finding org %s: %s", org.Id, err) return @@ -7622,7 +7593,7 @@ func handleStartHook(resp http.ResponseWriter, request *http.Request) { return } - user, err := handleApiAuthentication(resp, request) + user, err := shuffle.HandleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in set new workflowhandler: %s", err) resp.WriteHeader(401) @@ -7730,7 +7701,7 @@ func removeOutlookTriggerFunction(ctx context.Context, triggerId string) error { return nil } -func handleUserInput(trigger Trigger, organizationId string, workflowId string, referenceExecution string) error { +func handleUserInput(trigger shuffle.Trigger, organizationId string, workflowId string, referenceExecution string) error { // E.g. check email sms := "" email := "" @@ -7768,7 +7739,7 @@ func handleUserInput(trigger Trigger, organizationId string, workflowId string, FifthItem: referenceExecution, } - org, err := getOrg(ctx, organizationId) + org, err := shuffle.GetOrg(ctx, organizationId) if err != nil { log.Printf("Failed email send to cloud (1): %s", err) return err @@ -7794,7 +7765,7 @@ func handleUserInput(trigger Trigger, organizationId string, workflowId string, FifthItem: referenceExecution, } - org, err := getOrg(ctx, organizationId) + org, err := shuffle.GetOrg(ctx, organizationId) if err != nil { log.Printf("Failed sms send to cloud (3): %s", err) return err From 7e422c4d8b76334d6e7762ab1e3cfa0c0f2d3bd5 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 23 Mar 2021 09:58:04 +0100 Subject: [PATCH 174/185] More cloud sync migrations --- backend/go-app/go.mod | 4 +- backend/go-app/main.go | 658 +-------- backend/go-app/walkoff.go | 1878 +----------------------- frontend/src/views/AngularWorkflow.jsx | 31 +- frontend/src/views/Workflows.jsx | 3 +- functions/onprem/worker/worker.go | 2 +- 6 files changed, 110 insertions(+), 2466 deletions(-) diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index e403c8bd..f41eccf3 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -3,7 +3,6 @@ module shuffle go 1.13 replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared - replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi require ( @@ -13,14 +12,13 @@ require ( cloud.google.com/go/storage v1.12.0 github.com/Microsoft/go-winio v0.4.14 // indirect github.com/basgys/goxml2json v1.1.0 + github.com/frikky/kin-openapi v0.38.0 github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect github.com/frikky/shuffle-shared v0.0.12 // indirect - github.com/getkin/kin-openapi v0.52.0 // indirect - //github.com/getkin/kin-openapi v0.8.0 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0 diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 11df5536..01676100 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -63,7 +63,6 @@ import ( // Web "github.com/gorilla/mux" - "github.com/patrickmn/go-cache" "google.golang.org/api/option" "google.golang.org/grpc" http2 "gopkg.in/src-d/go-git.v4/plumbing/transport/http" @@ -84,7 +83,6 @@ var syncSubUrl = "https://shuffler.io" //var syncSubUrl = "https://050196912a9d.ngrok.io" var dbclient *datastore.Client -var requestCache *cache.Cache type Userapi struct { Username string `datastore:"username"` @@ -667,36 +665,6 @@ func parseLoginParameters(resp http.ResponseWriter, request *http.Request) (logi return t, nil } -// Can check against HIBP etc? -// Removed for localhost -func checkPasswordStrength(password string) error { - // Check password strength here - if len(password) < 3 { - return errors.New("Minimum password length is 3.") - } - - //if len(password) > 128 { - // return errors.New("Maximum password length is 128.") - //} - - //re := regexp.MustCompile("[0-9]+") - //if len(re.FindAllString(password, -1)) == 0 { - // return errors.New("Password must contain a number") - //} - - //re = regexp.MustCompile("[a-z]+") - //if len(re.FindAllString(password, -1)) == 0 { - // return errors.New("Password must contain a lower case char") - //} - - //re = regexp.MustCompile("[A-Z]+") - //if len(re.FindAllString(password, -1)) == 0 { - // return errors.New("Password must contain an upper case char") - //} - - return nil -} - func deleteUser(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -959,7 +927,7 @@ func handleSetEnvironments(resp http.ResponseWriter, request *http.Request) { func createNewUser(username, password, role, apikey string, org shuffle.Org) error { // Returns false if there is an issue // Use this for register - err := checkPasswordStrength(password) + err := shuffle.CheckPasswordStrength(password) if err != nil { log.Printf("Bad password strength: %s", err) return err @@ -1145,104 +1113,6 @@ func handleCookie(request *http.Request) bool { return true } -func handleLogout(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - http.SetCookie(resp, &http.Cookie{ - Name: "session_token", - Value: "", - Path: "/", - Expires: time.Unix(0, 0), - }) - - userInfo, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in handleLogout: %s", err) - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true, "reason": "Not logged in"}`)) - return - } - - ctx := context.Background() - session, err := shuffle.GetSession(ctx, userInfo.Session) - if err != nil { - log.Printf("Session %#v doesn't exist: %s", session, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "No session"}`)) - return - } - - // Check cookie - //c, err := request.Cookie("session_token") - //if err != nil { - // resp.WriteHeader(200) - // resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - // return - //} else { - // log.Printf("Session cookie is set to %s!", c.Value) - //} - - //var Userdata User - //ctx := context.Background() - //sessionToken = c.Value - //session, err := getSession(ctx, sessionToken) - //if err != nil { - // log.Printf("[WARNING] Session %s doesn't exist (logout): %s", sessionToken, err) - // resp.WriteHeader(401) - // resp.Write([]byte(`{"success": false, "reason": "Couldn't find your session"}`)) - // return - //} - - // Get session first - // Should basically never happen - //_, err = shuffle.GetUser(ctx, session.Id) - //if err != nil { - // log.Printf("Username %s doesn't exist (logout): %s", session.Username, err) - // resp.WriteHeader(401) - // resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - // return - //} - - // Userdata = *tmpdata - //} - - // FIXME - // Session might delete someone elses here? - // No need to think about before possible scale..? - err = shuffle.SetSession(ctx, userInfo, "") - if err != nil { - log.Printf("Error removing session for: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - err = DeleteKey(ctx, "sessions", userInfo.Session) - if err != nil { - log.Printf("Error deleting key %s for %s: %s", userInfo.Session, userInfo.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - userInfo.Session = "" - err = shuffle.SetUser(ctx, &userInfo) - if err != nil { - log.Printf("Failed updating user: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed updating apikey"}`)) - return - } - - //memcache.Delete(request.Context(), sessionToken) - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": false, "reason": "Successfully logged out"}`)) -} - func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -1370,105 +1240,6 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) } -func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - userInfo, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in apigen: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - ctx := context.Background() - if request.Method == "GET" { - newUserInfo, err := shuffle.GenerateApikey(ctx, userInfo) - if err != nil { - log.Printf("Failed to generate apikey for user %s: %s", userInfo.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - userInfo = newUserInfo - log.Printf("Updated apikey for user %s", userInfo.Username) - } else if request.Method == "POST" { - log.Printf("Handling post!") - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Println("Failed reading body") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Missing field: user_id"}`))) - return - } - - type userId struct { - UserId string `json:"user_id"` - } - - var t userId - err = json.Unmarshal(body, &t) - if err != nil { - log.Printf("Failed unmarshaling userId: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unmarshaling. Missing field: user_id"}`))) - return - } - - if userInfo.Role != "admin" { - log.Printf("%s tried and failed to change apikey for %s", userInfo.Username, t.UserId) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "You need to be admin to change others' apikey"}`))) - return - } - - foundUser, err := shuffle.GetUser(ctx, t.UserId) - if err != nil { - log.Printf("Can't find user %s (apikey gen): %s", t.UserId, err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) - return - } - - newUserInfo, err := shuffle.GenerateApikey(ctx, *foundUser) - if err != nil { - log.Printf("Failed to generate apikey for user %s: %s", foundUser.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - foundUser = &newUserInfo - - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, foundUser.Username, foundUser.Verified, foundUser.ApiKey))) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, userInfo.Username, userInfo.Verified, userInfo.ApiKey))) -} - -func handleSettings(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - userInfo, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in apigen: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, userInfo.Username, userInfo.Verified, userInfo.ApiKey))) -} - func handleInfo(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -1645,13 +1416,6 @@ type passwordReset struct { Reference string `json:"reference"` } -type passwordChange struct { - Username string `json:"username"` - Newpassword string `json:"newpassword"` - Newpassword2 string `json:"newpassword2"` - Currentpassword string `json:"currentpassword"` -} - func handlePasswordReset(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -1740,159 +1504,6 @@ func handlePasswordReset(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "%s"}`, defaultMessage))) } -func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - log.Println("Handling password change") - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Println("Failed reading body") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) - return - } - - // Get the current user - check if they're admin or the "username" user. - var t passwordChange - err = json.Unmarshal(body, &t) - if err != nil { - log.Println("Failed unmarshaling") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) - return - } - - userInfo, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in set new workflowhandler: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - curUserFound := false - if t.Username != userInfo.Username && userInfo.Role != "admin" { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Admin required to change others' passwords"}`)) - return - } else if t.Username == userInfo.Username { - curUserFound = true - } - - if userInfo.Role != "admin" { - if t.Newpassword != t.Newpassword2 { - err := "Passwords don't match" - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - if len(t.Newpassword) < 10 || len(t.Newpassword2) < 10 { - err := "Passwords too short - 2" - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - } else { - // Check ORG HERE? - } - - // Current password - err = checkPasswordStrength(t.Newpassword) - if err != nil { - log.Printf("Bad password strength: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - ctx := context.Background() - foundUser := shuffle.User{} - if !curUserFound { - log.Printf("Have to find a different user") - q := datastore.NewQuery("Users").Filter("Username =", strings.ToLower(t.Username)) - var users []shuffle.User - _, err = dbclient.GetAll(ctx, q, &users) - if err != nil { - log.Printf("Failed getting user %s", t.Username) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - if len(users) != 1 { - log.Printf(`Found multiple or no users with the same username: %s: %d`, t.Username, len(users)) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found %d users with the same username: %s"}`, len(users), t.Username))) - return - } - - foundUser = users[0] - orgFound := false - if userInfo.ActiveOrg.Id == foundUser.ActiveOrg.Id { - orgFound = true - } else { - log.Printf("FoundUser: %#v", foundUser.Orgs) - for _, item := range foundUser.Orgs { - if item == userInfo.ActiveOrg.Id { - orgFound = true - break - } - } - } - - if !orgFound { - log.Printf("User %s is admin, but can't change user's passowrd outside their own org.", userInfo.Id) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't change users outside your org."}`))) - return - } - } else { - // Admins can re-generate others' passwords as well. - if userInfo.Role != "admin" { - err = bcrypt.CompareHashAndPassword([]byte(userInfo.Password), []byte(t.Newpassword)) - if err != nil { - log.Printf("Bad password for %s: %s", userInfo.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - } - } - - if len(foundUser.Id) == 0 { - log.Printf("Something went wrong in password reset: couldn't find user.") - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false}`)) - return - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(t.Newpassword), 8) - if err != nil { - log.Printf("New password failure for %s: %s", userInfo.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - userInfo.Password = string(hashedPassword) - err = shuffle.SetUser(ctx, &foundUser) - if err != nil { - log.Printf("Error fixing password for user %s: %s", userInfo.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - //memcache.Delete(ctx, sessionToken) - - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) -} - // FIXME - forward this to emails or whatever CRM system in use func handleContact(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) @@ -2162,65 +1773,6 @@ func handleGetOrgs(resp http.ResponseWriter, request *http.Request) { resp.Write(newjson) } -func handleGetUsers(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in set new workflowhandler: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if user.Role != "admin" { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Not admin"}`)) - return - } - - // FIXME: Check by org. - ctx := context.Background() - org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) - if err != nil { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed getting org users"}`)) - return - } - - newUsers := []shuffle.User{} - for _, item := range org.Users { - if len(item.Username) == 0 { - continue - } - - //for _, tmpUser := range newUsers { - // if tmpUser.Name - //} - - item.Password = "" - item.Session = "" - item.VerificationToken = "" - item.Orgs = []string{} - - newUsers = append(newUsers, item) - } - - newjson, err := json.Marshal(newUsers) - if err != nil { - log.Printf("Failed unmarshal: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unpacking"}`))) - return - } - - resp.WriteHeader(200) - resp.Write(newjson) -} - func checkAdminLogin(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -2307,7 +1859,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { // FIXME - have timeout here loginData := `{"success": true}` if len(Userdata.Session) != 0 { - log.Println("[INFO] User session exists - resetting") + log.Println("[INFO] User session already exists - resetting it") expiration := time.Now().Add(3600 * time.Second) http.SetCookie(resp, &http.Cookie{ @@ -3350,7 +2902,7 @@ func executeCloudAction(action CloudSyncJob, apikey string) error { // Starts a new webhook func handleNewHook(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) + cors := shuffle.HandleCors(resp, request) if cors { return } @@ -3506,137 +3058,6 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": true}`)) } -func sendHookResult(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in set new workflowhandler: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - _ = user - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - } - - if len(workflowId) != 32 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "message": "ID not valid"}`)) - return - } - - ctx := context.Background() - hook, err := getHook(ctx, workflowId) - if err != nil { - log.Printf("Failed getting hook %s (send): %s", workflowId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Body data error: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - log.Printf("SET the hook results for %s to %s", workflowId, body) - // FIXME - set the hook result in the DB somehow as interface{} - // FIXME - should the hook do the transform? Hmm - - b, err := json.Marshal(hook) - if err != nil { - log.Printf("Failed marshalling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(b)) - return -} - -func handleGetHook(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in set new workflowhandler: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - } - - if len(workflowId) != 36 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "message": "ID not valid"}`)) - return - } - - ctx := context.Background() - hook, err := getHook(ctx, workflowId) - if err != nil { - log.Printf("Failed getting hook %s (get hook): %s", workflowId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if user.Id != hook.Owner && user.Role != "admin" && user.Role != "scheduler" { - log.Printf("Wrong user (%s) for hook %s", user.Username, hook.Id) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - b, err := json.Marshal(hook) - if err != nil { - log.Printf("Failed marshalling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - get some real data? - resp.WriteHeader(200) - resp.Write([]byte(b)) - return -} - func getSpecificSchedule(resp http.ResponseWriter, request *http.Request) { if request.Method != "GET" { setSpecificSchedule(resp, request) @@ -5465,9 +4886,9 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { } cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "id": "%s"}`, api.ID))) @@ -5533,7 +4954,7 @@ func createFs(basepath, pathname string) (billy.Filesystem, error) { } // Hotloads new apps from a folder -func handleAppHotload(location string, forceUpdate bool) error { +func handleAppHotload(ctx context.Context, location string, forceUpdate bool) error { basepath := "base" fs, err := createFs(basepath, location) @@ -5557,12 +4978,12 @@ func handleAppHotload(location string, forceUpdate bool) error { return err } - cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + cacheKey := fmt.Sprintf("workflowapps-sorted") + shuffle.DeleteCache(ctx, cacheKey) + cacheKey = fmt.Sprintf("workflowapps-sorted-100") + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) - cacheKey = fmt.Sprintf("workflowapps-sorted") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) return nil } @@ -5731,7 +5152,7 @@ func handleCloudJob(job CloudSyncJob) error { log.Printf("Should handle user_input CONTINUE for workflow %s with start node %s and execution ID %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) // FIXME: Handle authorization ctx := context.Background() - workflowExecution, err := getWorkflowExecution(ctx, job.ThirdItem) + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, job.ThirdItem) if err != nil { return err } @@ -5741,7 +5162,7 @@ func handleCloudJob(job CloudSyncJob) error { } workflowExecution.Status = "EXECUTING" - err = setWorkflowExecution(ctx, *workflowExecution, true) + err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) if err != nil { return err } @@ -5767,7 +5188,7 @@ func handleCloudJob(job CloudSyncJob) error { } else if job.Action == "stop" { log.Printf("Should handle user_input STOP for workflow %s with start node %s and execution ID %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) ctx := context.Background() - workflowExecution, err := getWorkflowExecution(ctx, job.ThirdItem) + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, job.ThirdItem) if err != nil { return err } @@ -5794,7 +5215,7 @@ func handleCloudJob(job CloudSyncJob) error { workflowExecution.Results = newResults workflowExecution.Status = "ABORTED" - err = setWorkflowExecution(ctx, *workflowExecution, true) + err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) if err != nil { return err } @@ -5925,7 +5346,7 @@ func runInit(ctx context.Context) { log.Printf("Running with HTTPS proxy %s (env: HTTPS_PROXY)", httpsProxy) } - requestCache = cache.New(5*time.Minute, 10*time.Minute) + //requestCache = cache.New(5*time.Minute, 10*time.Minute) /* proxyUrl, err := url.Parse(httpProxy) @@ -6201,7 +5622,7 @@ func runInit(ctx context.Context) { } if setLocal { - err = setWorkflow(ctx, workflow, workflow.ID) + err = shuffle.SetWorkflow(ctx, workflow, workflow.ID) if err != nil { log.Printf("Failed setting workflow in init: %s", err) } else { @@ -6444,7 +5865,7 @@ func runInit(ctx context.Context) { // Hotloads locally location := os.Getenv("SHUFFLE_APP_HOTLOAD_FOLDER") if len(location) != 0 { - handleAppHotload(location, false) + handleAppHotload(ctx, location, false) } } @@ -6713,7 +6134,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { return } - workflowExecution, err := getWorkflowExecution(ctx, tmpData.ExecutionRef) + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, tmpData.ExecutionRef) if err != nil { log.Printf("[INFO] User can't edit the org") resp.WriteHeader(401) @@ -7467,7 +6888,7 @@ func initHandlers() { ctx := context.Background() log.Printf("Starting Shuffle backend - initializing database connection") - requestCache = cache.New(5*time.Minute, 10*time.Minute) + //requestCache = cache.New(5*time.Minute, 10*time.Minute) dbclient, err = datastore.NewClient(ctx, gceProject, option.WithGRPCDialOption(grpc.WithNoProxy())) if err != nil { panic(fmt.Sprintf("DBclient error during init: %s", err)) @@ -7488,29 +6909,30 @@ func initHandlers() { // Make user related locations // Fix user changes with org - r.HandleFunc("/api/v1/users/generateapikey", handleApiGeneration).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/users/login", handleLogin).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/users/logout", handleLogout).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/register", handleRegister).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/checkusers", checkAdminLogin).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/getinfo", handleInfo).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/users/getsettings", handleSettings).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/users/getusers", handleGetUsers).Methods("GET", "OPTIONS") + + r.HandleFunc("/api/v1/users/generateapikey", shuffle.HandleApiGeneration).Methods("GET", "POST", "OPTIONS") + r.HandleFunc("/api/v1/users/logout", shuffle.HandleLogout).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/users/getsettings", shuffle.HandleSettings).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/users/getusers", shuffle.HandleGetUsers).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/updateuser", handleUpdateUser).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/users/{user}", deleteUser).Methods("DELETE", "OPTIONS") - r.HandleFunc("/api/v1/users/passwordchange", handlePasswordChange).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/users", handleGetUsers).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/users/passwordchange", shuffle.HandlePasswordChange).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/users", shuffle.HandleGetUsers).Methods("GET", "OPTIONS") // General - duplicates and old. + r.HandleFunc("/api/v1/getusers", shuffle.HandleGetUsers).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/login", handleLogin).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/logout", handleLogout).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/logout", shuffle.HandleLogout).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/register", handleRegister).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/checkusers", checkAdminLogin).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/getusers", handleGetUsers).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/getinfo", handleInfo).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/getsettings", handleSettings).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/generateapikey", handleApiGeneration).Methods("GET", "POST", "OPTIONS") - r.HandleFunc("/api/v1/passwordchange", handlePasswordChange).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/getsettings", shuffle.HandleSettings).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/generateapikey", shuffle.HandleApiGeneration).Methods("GET", "POST", "OPTIONS") + r.HandleFunc("/api/v1/passwordchange", shuffle.HandlePasswordChange).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/getenvironments", handleGetEnvironments).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/setenvironments", handleSetEnvironments).Methods("PUT", "OPTIONS") @@ -7542,11 +6964,11 @@ func initHandlers() { r.HandleFunc("/api/v1/apps", setNewWorkflowApp).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/apps/search", getSpecificApps).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/apps/authentication", getAppAuthentication).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/apps/authentication", addAppAuthentication).Methods("PUT", "OPTIONS") - r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", setAuthenticationConfig).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/apps/authentication", shuffle.GetAppAuthentication).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/apps/authentication", shuffle.AddAppAuthentication).Methods("PUT", "OPTIONS") + r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", shuffle.SetAuthenticationConfig).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/apps/authentication/{appauthId}", deleteAppAuthentication).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/apps/authentication/{appauthId}", shuffle.DeleteAppAuthentication).Methods("DELETE", "OPTIONS") // Legacy app things r.HandleFunc("/api/v1/workflows/apps/validate", validateAppInput).Methods("POST", "OPTIONS") @@ -7556,8 +6978,8 @@ func initHandlers() { // Workflows // FIXME - implement the queue counter lol /* Everything below here increases the counters*/ - r.HandleFunc("/api/v1/workflows", getWorkflows).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/workflows", setNewWorkflow).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/workflows", shuffle.GetWorkflows).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/workflows", shuffle.SetNewWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/schedules", handleGetSchedules).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/download_remote", loadSpecificWorkflows).Methods("POST", "OPTIONS") @@ -7567,8 +6989,8 @@ func initHandlers() { r.HandleFunc("/api/v1/workflows/{key}/outlook/{triggerId}", handleDeleteOutlookSub).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/executions", getWorkflowExecutions).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/executions/{key}/abort", abortExecution).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/workflows/{key}", getSpecificWorkflow).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/workflows/{key}", saveWorkflow).Methods("PUT", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}", shuffle.GetSpecificWorkflow).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}", shuffle.SaveWorkflow).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}", deleteWorkflow).Methods("DELETE", "OPTIONS") // Triggers diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 10b96f19..f20d15ee 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -44,7 +44,6 @@ import ( //"cloud.google.com/go/firestore" // "google.golang.org/api/option" - "github.com/patrickmn/go-cache" "google.golang.org/api/iterator" ) @@ -992,7 +991,7 @@ func validateNewWorkerExecution(body []byte) error { //} //log.Printf("\n\nSHOULD SET BACKEND DATA FOR EXEC \n\n") - err = setWorkflowExecution(ctx, execution, true) + err = shuffle.SetWorkflowExecution(ctx, execution, true) if err == nil { log.Printf("[INFO] Set workflowexecution based on new worker (>0.8.53) for execution %s. Actions: %d, Triggers: %d, Results: %d", execution.ExecutionId, len(execution.Workflow.Actions), len(execution.Workflow.Triggers), len(execution.Results)) //log.Printf("[INFO] Successfully set the execution to wait.") @@ -1102,7 +1101,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { actionResult.Result = fmt.Sprintf("Cloud error: %s", err) workflowExecution.Results = append(workflowExecution.Results, actionResult) workflowExecution.Status = "ABORTED" - err = setWorkflowExecution(ctx, *workflowExecution, true) + err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) if err != nil { log.Printf("Failed to set execution during wait") } else { @@ -1120,7 +1119,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { workflowExecution.Results = append(workflowExecution.Results, actionResult) workflowExecution.Status = actionResult.Status - err = setWorkflowExecution(ctx, *workflowExecution, true) + err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) if err != nil { log.Printf("Failed ") } else { @@ -1144,7 +1143,8 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution"}`))) return } - resultLength := len(workflowExecution.Results) + + //resultLength := len(workflowExecution.Results) dbSave := false setExecution := true @@ -1488,10 +1488,12 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // Validating that action results hasn't changed // Handled using cachhing, so actually pretty fast cacheKey := fmt.Sprintf("workflowexecution-%s", workflowExecution.ExecutionId) - if value, found := requestCache.Get(cacheKey); found { - parsedValue := value.(*shuffle.WorkflowExecution) - if len(parsedValue.Results) > 0 && len(parsedValue.Results) != resultLength { - setExecution = false + cache, err := shuffle.GetCache(ctx, cacheKey) + if err == nil { + cacheData := []byte(cache.([]uint8)) + //log.Printf("CACHEDATA: %#v", cacheData) + err = json.Unmarshal(cacheData, &workflowExecution) + if err == nil { if attempts > 5 { //log.Printf("\n\nSkipping execution input - %d vs %d. Attempts: (%d)\n\n", len(parsedValue.Results), resultLength, attempts) } @@ -1504,8 +1506,24 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl } } + //if value, found := requestCache.Get(cacheKey); found { + // parsedValue := value.(*shuffle.WorkflowExecution) + // if len(parsedValue.Results) > 0 && len(parsedValue.Results) != resultLength { + // setExecution = false + // if attempts > 5 { + // //log.Printf("\n\nSkipping execution input - %d vs %d. Attempts: (%d)\n\n", len(parsedValue.Results), resultLength, attempts) + // } + + // attempts += 1 + // if len(workflowExecution.Results) <= len(workflowExecution.Workflow.Actions) { + // runWorkflowExecutionTransaction(ctx, attempts, workflowExecutionId, actionResult, resp) + // return + // } + // } + //} + if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { - err = setWorkflowExecution(ctx, *workflowExecution, dbSave) + err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, dbSave) if err != nil { resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed setting workflowexecution actionresult: %s"}`, err))) @@ -1540,7 +1558,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl // log.Printf("[ERROR] QUITTING: tx.Commit %d: %v", attempts, err) // workflowExecution.Status = "ABORTED" - // setWorkflowExecution(ctx, *workflowExecution, true) + // shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) // resp.WriteHeader(401) // resp.Write([]byte(`{"success": false}`)) @@ -1741,233 +1759,6 @@ func getWorkflows(resp http.ResponseWriter, request *http.Request) { resp.Write(newjson) } -// FIXME - add to actual database etc -func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in set new workflowhandler: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Error with body read: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - var workflow shuffle.Workflow - err = json.Unmarshal(body, &workflow) - if err != nil { - log.Printf("Failed unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflow.ID = uuid.NewV4().String() - workflow.Owner = user.Id - workflow.Sharing = "private" - user.ActiveOrg.Users = []shuffle.User{} - workflow.ExecutingOrg = user.ActiveOrg - workflow.OrgId = user.ActiveOrg.Id - //log.Printf("TRIGGERS: %d", len(workflow.Triggers)) - - ctx := context.Background() - //err = increaseStatisticsField(ctx, "total_workflows", workflow.ID, 1, workflow.OrgId) - //if err != nil { - // log.Printf("Failed to increase total workflows stats: %s", err) - //} - - if len(workflow.Actions) == 0 { - workflow.Actions = []shuffle.Action{} - } - if len(workflow.Branches) == 0 { - workflow.Branches = []shuffle.Branch{} - } - if len(workflow.Triggers) == 0 { - workflow.Triggers = []shuffle.Trigger{} - } - if len(workflow.Errors) == 0 { - workflow.Errors = []string{} - } - - newActions := []shuffle.Action{} - for _, action := range workflow.Actions { - if action.Environment == "" { - //action.Environment = baseEnvironment - action.IsValid = true - } - - //action.LargeImage = "" - newActions = append(newActions, action) - } - - // Initialized without functions = adding a hello world node. - if len(newActions) == 0 { - //log.Printf("APPENDING NEW APP FOR NEW WORKFLOW") - - // Adds the Testing app if it's a new workflow - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) - if err == nil { - // FIXME: Add real env - envName := "Shuffle" - environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id) - if err == nil { - for _, env := range environments { - if env.Default { - envName = env.Name - break - } - } - } - - for _, item := range workflowapps { - if item.Name == "Testing" && item.AppVersion == "1.0.0" { - nodeId := "40447f30-fa44-4a4f-a133-4ee710368737" - workflow.Start = nodeId - newActions = append(newActions, shuffle.Action{ - Label: "Start node", - Name: "hello_world", - Environment: envName, - Parameters: []shuffle.WorkflowAppActionParameter{}, - Position: struct { - X float64 "json:\"x,omitempty\" datastore:\"x\"" - Y float64 "json:\"y,omitempty\" datastore:\"y\"" - }{X: 449.5, Y: 446}, - Priority: 0, - Errors: []string{}, - ID: nodeId, - IsValid: true, - IsStartNode: true, - Sharing: true, - PrivateID: "", - SmallImage: "", - AppName: item.Name, - AppVersion: item.AppVersion, - AppID: item.ID, - LargeImage: item.LargeImage, - }) - break - } - } - } - } else { - log.Printf("[INFO] Has %d actions already", len(newActions)) - // FIXME: Check if they require authentication and if they exist locally - //log.Printf("\n\nSHOULD VALIDATE AUTHENTICATION") - //AuthenticationId string `json:"authentication_id,omitempty" datastore:"authentication_id"` - //allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - //if err == nil { - // log.Printf("AUTH: %#v", allAuths) - // for _, action := range newActions { - // log.Printf("ACTION: %#v", action) - // } - //} - } - - workflow.Actions = []shuffle.Action{} - for _, item := range workflow.Actions { - oldId := item.ID - sourceIndexes := []int{} - destinationIndexes := []int{} - for branchIndex, branch := range workflow.Branches { - if branch.SourceID == oldId { - sourceIndexes = append(sourceIndexes, branchIndex) - } - - if branch.DestinationID == oldId { - destinationIndexes = append(destinationIndexes, branchIndex) - } - } - - item.ID = uuid.NewV4().String() - for _, index := range sourceIndexes { - workflow.Branches[index].SourceID = item.ID - } - - for _, index := range destinationIndexes { - workflow.Branches[index].DestinationID = item.ID - } - - newActions = append(newActions, item) - } - - newTriggers := []shuffle.Trigger{} - for _, item := range workflow.Triggers { - oldId := item.ID - sourceIndexes := []int{} - destinationIndexes := []int{} - for branchIndex, branch := range workflow.Branches { - if branch.SourceID == oldId { - sourceIndexes = append(sourceIndexes, branchIndex) - } - - if branch.DestinationID == oldId { - destinationIndexes = append(destinationIndexes, branchIndex) - } - } - - item.ID = uuid.NewV4().String() - for _, index := range sourceIndexes { - workflow.Branches[index].SourceID = item.ID - } - - for _, index := range destinationIndexes { - workflow.Branches[index].DestinationID = item.ID - } - - item.Status = "uninitialized" - newTriggers = append(newTriggers, item) - } - - newSchedules := []shuffle.Schedule{} - for _, item := range workflow.Schedules { - item.Id = uuid.NewV4().String() - newSchedules = append(newSchedules, item) - } - - timeNow := int64(time.Now().Unix()) - workflow.Actions = newActions - workflow.Triggers = newTriggers - workflow.Schedules = newSchedules - workflow.IsValid = true - workflow.Configuration.ExitOnError = false - workflow.Created = timeNow - - workflowjson, err := json.Marshal(workflow) - if err != nil { - log.Printf("Failed workflow json setting marshalling: %s", err) - resp.WriteHeader(http.StatusInternalServerError) - resp.Write([]byte(`{"success": false}`)) - return - } - - err = setWorkflow(ctx, workflow, workflow.ID) - if err != nil { - log.Printf("Failed setting workflow: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - log.Printf("[INFO] Saved new workflow %s with name %s", workflow.ID, workflow.Name) - //memcacheName := fmt.Sprintf("%s_workflows", user.Username) - //memcache.Delete(ctx, memcacheName) - - resp.WriteHeader(200) - //log.Println(string(workflowjson)) - resp.Write(workflowjson) -} - func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -2066,875 +1857,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": true}`)) } -// Adds app auth tracking -func updateAppAuth(auth shuffle.AppAuthenticationStorage, workflowId, nodeId string, add bool) error { - workflowFound := false - workflowIndex := 0 - nodeFound := false - for index, workflow := range auth.Usage { - if workflow.WorkflowId == workflowId { - // Check if node exists - workflowFound = true - workflowIndex = index - for _, actionId := range workflow.Nodes { - if actionId == nodeId { - nodeFound = true - break - } - } - - break - } - } - - // FIXME: Add a way to use !add to remove - updateAuth := false - if !workflowFound && add { - log.Printf("[INFO] Adding workflow things to auth!") - usageItem := shuffle.AuthenticationUsage{ - WorkflowId: workflowId, - Nodes: []string{nodeId}, - } - - auth.Usage = append(auth.Usage, usageItem) - auth.WorkflowCount += 1 - auth.NodeCount += 1 - updateAuth = true - } else if !nodeFound && add { - log.Printf("[INFO] Adding node things to auth!") - auth.Usage[workflowIndex].Nodes = append(auth.Usage[workflowIndex].Nodes, nodeId) - auth.NodeCount += 1 - updateAuth = true - } - - if updateAuth { - log.Printf("[INFO] Updating auth!") - ctx := context.Background() - err := shuffle.SetWorkflowAppAuthDatastore(ctx, auth, auth.Id) - if err != nil { - log.Printf("Failed setting up app auth %s: %s", auth.Id, err) - return err - } - } - - return nil -} - // Identifies what a category defined really is -func handleCategoryIncrease(categories shuffle.Categories, action shuffle.Action, workflowapps []shuffle.WorkflowApp) shuffle.Categories { - if action.Category == "" { - appName := action.AppName - for _, app := range workflowapps { - if appName != strings.ToLower(app.Name) { - continue - } - - if len(app.Categories) > 0 { - log.Printf("[INFO] Setting category for %s: %s", app.Name, app.Categories) - action.Category = app.Categories[0] - break - } - } - - //log.Printf("Should find app's categories as it's empty during save") - return categories - } - - //log.Printf("Action: %s, category: %s", action.AppName, action.Category) - // FIXME: Make this an "autodiscover" that's controlled by the category itself - // Should just be a list that's looped against :) - newCategory := strings.ToLower(action.Category) - if strings.Contains(newCategory, "case") || strings.Contains(newCategory, "ticket") || strings.Contains(newCategory, "alert") || strings.Contains(newCategory, "mssp") { - categories.Cases.Count += 1 - } else if strings.Contains(newCategory, "siem") || strings.Contains(newCategory, "event") || strings.Contains(newCategory, "log") || strings.Contains(newCategory, "search") { - categories.SIEM.Count += 1 - } else if strings.Contains(newCategory, "sms") || strings.Contains(newCategory, "comm") || strings.Contains(newCategory, "phone") || strings.Contains(newCategory, "call") || strings.Contains(newCategory, "chat") || strings.Contains(newCategory, "mail") || strings.Contains(newCategory, "phish") { - categories.Communication.Count += 1 - } else if strings.Contains(newCategory, "intel") || strings.Contains(newCategory, "crim") || strings.Contains(newCategory, "ti") { - categories.Intel.Count += 1 - } else if strings.Contains(newCategory, "sand") || strings.Contains(newCategory, "virus") || strings.Contains(newCategory, "malware") || strings.Contains(newCategory, "scan") || strings.Contains(newCategory, "edr") || strings.Contains(newCategory, "endpoint detection") { - // Sandbox lol - categories.EDR.Count += 1 - } else if strings.Contains(newCategory, "vuln") || strings.Contains(newCategory, "fim") || strings.Contains(newCategory, "fim") || strings.Contains(newCategory, "integrity") { - categories.Assets.Count += 1 - } else if strings.Contains(newCategory, "network") || strings.Contains(newCategory, "firewall") || strings.Contains(newCategory, "waf") || strings.Contains(newCategory, "switch") { - categories.Network.Count += 1 - } else { - categories.Other.Count += 1 - } - - return categories -} - -// Saves a workflow to an ID -func saveWorkflow(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - //log.Println("Start") - user, userErr := shuffle.HandleApiAuthentication(resp, request) - if userErr != nil { - log.Printf("Api authentication failed in edit workflow: %s", userErr) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - //log.Println("PostUser") - location := strings.Split(request.URL.String(), "/") - - var fileId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[4] - } - - if len(fileId) != 36 { - log.Printf(`ID %s is not valid`, fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Workflow ID to save is not valid"}`)) - return - } - - // Here to check access rights - ctx := context.Background() - tmpworkflow, err := shuffle.GetWorkflow(ctx, fileId) - if err != nil { - log.Printf("Failed getting the workflow locally (save workflow): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - have a check for org etc too.. - if user.Id != tmpworkflow.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for workflow %s (save)", user.Username, tmpworkflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - //log.Printf("PRE BODY") - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Failed hook unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - var workflow shuffle.Workflow - err = json.Unmarshal([]byte(body), &workflow) - //log.Printf(string(body)) - if err != nil { - log.Printf(string(body)) - log.Printf("[ERROR] Failed workflow unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - //log.Printf("SAVED: %#v", workflow.PreviouslySaved) - - // FIXME - auth and check if they should have access - if fileId != workflow.ID { - log.Printf("Path and request ID are not matching: %s:%s.", fileId, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Fixing wrong owners when importing - if workflow.Owner == "" { - workflow.Owner = user.Id - } - - if len(workflow.ExecutingOrg.Id) == 0 { - log.Printf("[INFO] Setting executing org for workflow") - user.ActiveOrg.Users = []shuffle.User{} - workflow.ExecutingOrg = user.ActiveOrg - } - - // FIXME - this shouldn't be necessary with proper API checks - newActions := []shuffle.Action{} - allNodes := []string{} - workflow.Categories = shuffle.Categories{} - - //log.Printf("PRE APPS") - workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 500) - - //log.Printf("Action: %#v", action.Authentication) - for _, action := range workflow.Actions { - allNodes = append(allNodes, action.ID) - - if len(action.Errors) > 0 || !action.IsValid { - action.IsValid = true - action.Errors = []string{} - } - - if action.Environment == "" { - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "An environment for %s is required"}`, action.Label))) - return - } - action.IsValid = true - } - - // FIXME: Have a good way of tracking errors. ID's or similar. - if !action.IsValid && len(action.Errors) > 0 { - log.Printf("Node %s is invalid and needs to be remade. Errors: %s", action.Label, strings.Join(action.Errors, "\n")) - - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Node %s is invalid and needs to be remade."}`, action.Label))) - return - } - action.IsValid = true - action.Errors = []string{} - } - - workflow.Categories = handleCategoryIncrease(workflow.Categories, action, workflowapps) - newActions = append(newActions, action) - } - - newTriggers := []shuffle.Trigger{} - for _, trigger := range workflow.Triggers { - log.Printf("[INFO] Trigger %s: %s", trigger.TriggerType, trigger.Status) - - // Check if it's actually running - // FIXME: Do this for other triggers too - if trigger.TriggerType == "SCHEDULE" && trigger.Status != "uninitialized" { - schedule, err := shuffle.GetSchedule(ctx, trigger.ID) - if err != nil { - trigger.Status = "stopped" - } else if schedule.Id == "" { - trigger.Status = "stopped" - } - } else if trigger.TriggerType == "SUBFLOW" { - for index, param := range trigger.Parameters { - if len(param.Value) == 0 && param.Name != "argument" { - //log.Printf("Param: %#v", param) - if param.Name == "user_apikey" { - apikey := "" - if len(user.ApiKey) > 0 { - apikey = user.ApiKey - } else { - user, err = shuffle.GenerateApikey(ctx, user) - if err != nil { - workflow.IsValid = false - workflow.Errors = []string{"Trigger is missing a parameter: %s", param.Name} - - log.Printf("No type specified for user input node") - - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Trigger %s is missing the parameter %s"}`, trigger.Label, param.Name))) - return - } - } - - apikey = user.ApiKey - } - - log.Printf("[INFO] Set apikey in subflow trigger for user during save") - trigger.Parameters[index].Value = apikey - } else { - - workflow.IsValid = false - workflow.Errors = []string{"Trigger is missing a parameter: %s", param.Name} - - log.Printf("No type specified for user input node") - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Trigger %s is missing the parameter %s"}`, trigger.Label, param.Name))) - return - } - } - } - } - } else if trigger.TriggerType == "WEBHOOK" && trigger.Status != "uninitialized" { - hook, err := getHook(ctx, trigger.ID) - if err != nil { - log.Printf("Failed getting webhook") - trigger.Status = "stopped" - } else if hook.Id == "" { - trigger.Status = "stopped" - } - } else if trigger.TriggerType == "USERINPUT" { - // E.g. check email - sms := "" - email := "" - triggerType := "" - triggerInformation := "" - for _, item := range trigger.Parameters { - if item.Name == "alertinfo" { - triggerInformation = item.Value - } else if item.Name == "type" { - triggerType = item.Value - } else if item.Name == "email" { - email = item.Value - } else if item.Name == "sms" { - sms = item.Value - } - } - - if len(triggerType) == 0 { - log.Printf("No type specified for user input node") - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No contact option specified in user input"}`))) - return - } - } - - // FIXME: This is not the right time to send them, BUT it's well served for testing. Save -> send email / sms - _ = triggerInformation - if strings.Contains(triggerType, "email") { - if email == "test@test.com" { - log.Printf("Email isn't specified during save.") - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Email field in user input can't be empty"}`))) - return - } - } - - log.Printf("Should send email to %s during execution.", email) - } - if strings.Contains(triggerType, "sms") { - if sms == "0000000" { - log.Printf("Email isn't specified during save.") - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "SMS field in user input can't be empty"}`))) - return - } - } - - log.Printf("Should send SMS to %s during execution.", sms) - } - } - - //log.Println("TRIGGERS") - allNodes = append(allNodes, trigger.ID) - newTriggers = append(newTriggers, trigger) - } - - workflow.Triggers = newTriggers - - if len(workflow.Actions) == 0 { - workflow.Actions = []shuffle.Action{} - } - if len(workflow.Branches) == 0 { - workflow.Branches = []shuffle.Branch{} - } - if len(workflow.Triggers) == 0 { - workflow.Triggers = []shuffle.Trigger{} - } - if len(workflow.Errors) == 0 { - workflow.Errors = []string{} - } - - //log.Printf("PRE VARIABLES") - for _, variable := range workflow.WorkflowVariables { - if len(variable.Value) == 0 { - log.Printf("[WARNING] Variable %s is empty!", variable.Name) - workflow.Errors = append(workflow.Errors, fmt.Sprintf("Variable %s is empty!", variable.Name)) - //resp.WriteHeader(401) - //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Variable %s can't be empty"}`, variable.Name))) - //return - } - } - - if len(workflow.ExecutionVariables) > 0 { - log.Printf("[INFO] Found %d execution variable(s)", len(workflow.ExecutionVariables)) - } - - if len(workflow.WorkflowVariables) > 0 { - log.Printf("[INFO] Found %d workflow variable(s)", len(workflow.WorkflowVariables)) - } - - // FIXME - do actual checks ROFL - // FIXME - minor issues with e.g. hello world and self.console_logger - // Nodechecks - foundNodes := []string{} - for _, node := range allNodes { - for _, branch := range workflow.Branches { - //log.Println("branch") - //log.Println(node) - //log.Println(branch.DestinationID) - if node == branch.DestinationID || node == branch.SourceID { - foundNodes = append(foundNodes, node) - break - } - } - } - - // FIXME - append all nodes (actions, triggers etc) to one single array here - //log.Printf("PRE VARIABLES") - if len(foundNodes) != len(allNodes) || len(workflow.Actions) <= 0 { - // This shit takes a few seconds lol - if !workflow.IsValid { - oldworkflow, err := shuffle.GetWorkflow(ctx, fileId) - if err != nil { - log.Printf("Workflow %s doesn't exist - oldworkflow.", fileId) - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Item already exists."}`)) - return - } - } - - oldworkflow.IsValid = false - err = setWorkflow(ctx, *oldworkflow, fileId) - if err != nil { - log.Printf("Failed saving workflow to database: %s", err) - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - } - } - - // FIXME - more checks here - force reload of data or something - //if len(allNodes) == 0 { - // resp.WriteHeader(401) - // resp.Write([]byte(`{"success": false, "reason": "Please insert a node"}`)) - // return - //} - - // Allowed with only a start node - //if len(allNodes) != 1 { - // resp.WriteHeader(401) - // resp.Write([]byte(`{"success": false, "reason": "There are nodes with no branches"}`)) - // return - //} - } - - // FIXME - might be a sploit to run someone elses app if getAllWorkflowApps - // doesn't check sharing=true - // Have to do it like this to add the user's apps - //log.Println("Apps set starting") - //log.Printf("EXIT ON ERROR: %#v", workflow.Configuration.ExitOnError) - //workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 500) - - // Started getting the single apps, but if it's weird, this is faster - // 1. Check workflow.Start - // 2. Check if any node has "isStartnode" - if len(workflow.Actions) > 0 { - index := -1 - for indexFound, action := range workflow.Actions { - //log.Println("Apps set done") - if workflow.Start == action.ID { - index = indexFound - } - } - - if index >= 0 { - workflow.Actions[0].IsStartNode = true - } else { - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "You need to set a startnode."}`))) - return - } - } - } - - allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - if userErr != nil { - log.Printf("Api authentication failed in get all apps: %s", userErr) - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - } - - // Check every app action and param to see whether they exist - //log.Printf("PRE ACTIONS 2") - allAuths, autherr := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - newActions = []shuffle.Action{} - for _, action := range workflow.Actions { - reservedApps := []string{ - "0ca8887e-b4af-4e3e-887c-87e9d3bc3d3e", - } - - //log.Printf("%s Action execution var: %s", action.Label, action.ExecutionVariable.Name) - - builtin := false - for _, id := range reservedApps { - if id == action.AppID { - builtin = true - break - } - } - - // Check auth - // 1. Find the auth in question - // 2. Update the node and workflow info in the auth - // 3. Get the values in the auth and add them to the action values - if len(action.AuthenticationId) > 0 { - authFound := false - for _, auth := range allAuths { - if auth.Id == action.AuthenticationId { - authFound = true - - // Updates the auth item itself IF necessary - go updateAppAuth(auth, workflow.ID, action.ID, true) - break - } - } - - if !authFound { - log.Printf("App auth %s doesn't exist. Setting error", action.AuthenticationId) - workflow.Errors = append(workflow.Errors, fmt.Sprintf("App authentication for %s doesn't exist!", action.AppName)) - workflow.IsValid = false - - action.Errors = append(action.Errors, "App authentication doesn't exist") - action.IsValid = false - action.AuthenticationId = "" - //resp.WriteHeader(401) - //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "App auth %s doesn't exist"}`, action.AuthenticationId))) - //return - } - } - - if builtin { - newActions = append(newActions, action) - } else { - curapp := shuffle.WorkflowApp{} - // FIXME - can this work with ONLY AppID? - for _, app := range workflowapps { - if app.ID == action.AppID { - curapp = app - break - } - - // Has to NOT be generated - if app.Name == action.AppName && app.AppVersion == action.AppVersion { - curapp = app - break - } - } - - // Check to see if the whole app is valid - if curapp.Name != action.AppName { - workflow.Errors = append(workflow.Errors, fmt.Sprintf("App %s doesn't exist", action.AppName)) - action.Errors = append(action.Errors, "This app doesn't exist.") - action.IsValid = false - workflow.IsValid = false - - // Append with errors - newActions = append(newActions, action) - log.Printf("App %s doesn't exist. Adding as error.", action.AppName) - //resp.WriteHeader(401) - //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "App %s doesn't exist"}`, action.AppName))) - //return - } else { - // Check tosee if the appaction is valid - curappaction := shuffle.WorkflowAppAction{} - for _, curAction := range curapp.Actions { - if action.Name == curAction.Name { - curappaction = curAction - break - } - } - - // Check to see if the action is valid - if curappaction.Name != action.Name { - log.Printf("[ERROR] Action %s in app %s doesn't exist.", action.Name, curapp.Name) - thisError := fmt.Sprintf("%s: Action %s in app %s doesn't exist", action.Label, action.Name, action.AppName) - workflow.Errors = append(workflow.Errors, thisError) - workflow.IsValid = false - action.Errors = append(action.Errors, thisError) - action.IsValid = false - //if workflow.PreviouslySaved { - // resp.WriteHeader(401) - // resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Action %s in app %s doesn't exist"}`, action.Name, curapp.Name))) - // return - //} - } - - // FIXME - check all parameters to see if they're valid - // Includes checking required fields - - selectedAuth := shuffle.AppAuthenticationStorage{} - if len(action.AuthenticationId) > 0 && autherr == nil { - for _, auth := range allAuths { - if auth.Id == action.AuthenticationId { - selectedAuth = auth - break - } - } - } - - newParams := []shuffle.WorkflowAppActionParameter{} - for _, param := range curappaction.Parameters { - paramFound := false - - // Handles check for parameter exists + value not empty in used fields - for _, actionParam := range action.Parameters { - if actionParam.Name == param.Name { - paramFound = true - - if actionParam.Value == "" && actionParam.Variant == "STATIC_VALUE" && actionParam.Required == true { - // Validating if the field is an authentication field - if len(selectedAuth.Id) > 0 { - authFound := false - for _, field := range selectedAuth.Fields { - if field.Key == actionParam.Name { - authFound = true - //log.Printf("FOUND REQUIRED KEY %s IN AUTH", field.Key) - break - } - } - - if authFound { - newParams = append(newParams, actionParam) - continue - } - } - - log.Printf("[WARNING] Appaction %s with required param '%s' is empty. Can't save.", action.Name, param.Name) - thisError := fmt.Sprintf("%s is missing reqired parameter %s", action.Label, param.Name) - action.Errors = append(action.Errors, thisError) - workflow.Errors = append(workflow.Errors, thisError) - action.IsValid = false - } - - if actionParam.Variant == "" { - actionParam.Variant = "STATIC_VALUE" - } - - newParams = append(newParams, actionParam) - break - } - } - - // Handles check for required params - if !paramFound && param.Required { - log.Printf("Appaction %s with required param %s doesn't exist.", action.Name, param.Name) - thisError := fmt.Sprintf("Parameter %s is required", param.Name) - action.Errors = append(action.Errors, thisError) - - workflow.Errors = append(workflow.Errors, thisError) - action.IsValid = false - //newActions = append(newActions, action) - //resp.WriteHeader(401) - //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Appaction %s with required param '%s' is empty."}`, action.Name, param.Name))) - //return - } - - } - - action.Parameters = newParams - newActions = append(newActions, action) - } - } - } - - if !workflow.PreviouslySaved { - log.Printf("[WORKFLOW INIT] NOT PREVIOUSLY SAVED - SET ACTION AUTH!") - - if autherr == nil && len(workflowapps) > 0 && apperr == nil { - //log.Printf("Setting actions") - actionFixing := []shuffle.Action{} - appsAdded := []string{} - for _, action := range newActions { - setAuthentication := false - if len(action.AuthenticationId) > 0 { - //found := false - authenticationFound := false - for _, auth := range allAuths { - if auth.Id == action.AuthenticationId { - authenticationFound = true - break - } - } - - if !authenticationFound { - setAuthentication = true - } - } else { - // FIXME: 1. Validate if the app needs auth - // 1. Validate if auth for the app exists - // var appAuth AppAuthenticationStorage - setAuthentication = true - - //App WorkflowApp `json:"app" datastore:"app,noindex"` - } - - if setAuthentication { - authSet := false - for _, auth := range allAuths { - if !auth.Active { - continue - } - - if !auth.Defined { - continue - } - - if auth.App.Name == action.AppName { - //log.Printf("FOUND AUTH FOR APP %s: %s", auth.App.Name, auth.Id) - action.AuthenticationId = auth.Id - authSet = true - break - } - } - - // FIXME: Only o this IF there isn't another one for the app already - if !authSet { - //log.Printf("Validate if the app NEEDS auth or not") - outerapp := shuffle.WorkflowApp{} - for _, app := range workflowapps { - if app.Name == action.AppName { - outerapp = app - break - } - } - - if len(outerapp.ID) > 0 && outerapp.Authentication.Required { - found := false - for _, auth := range allAuths { - if auth.App.ID == outerapp.ID { - found = true - break - } - } - - for _, added := range appsAdded { - if outerapp.ID == added { - found = true - } - } - - // FIXME: Add app auth - if !found { - timeNow := int64(time.Now().Unix()) - authFields := []shuffle.AuthenticationStore{} - for _, param := range outerapp.Authentication.Parameters { - authFields = append(authFields, shuffle.AuthenticationStore{ - Key: param.Name, - Value: "", - }) - } - - appAuth := shuffle.AppAuthenticationStorage{ - Active: true, - Label: fmt.Sprintf("default_%s", outerapp.Name), - Id: uuid.NewV4().String(), - App: outerapp, - Fields: authFields, - Usage: []shuffle.AuthenticationUsage{}, - WorkflowCount: 0, - NodeCount: 0, - OrgId: user.ActiveOrg.Id, - Created: timeNow, - Edited: timeNow, - } - - err = shuffle.SetWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) - if err != nil { - log.Printf("Failed setting appauth for with name %s", appAuth.Label) - } else { - appsAdded = append(appsAdded, outerapp.ID) - } - } - - action.Errors = append(action.Errors, "Requires authentication") - action.IsValid = false - workflow.IsValid = false - } - - //outerapp.Authentication.Required - // Authentication Authentication `json:"authentication" yaml:"authentication" required:false datastore:"authentication"` - //workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 100) - } - } - - actionFixing = append(actionFixing, action) - } - - newActions = actionFixing - } else { - log.Printf("FirstSave error: %s - %s", err, apperr) - //workflowapps, apperr := shuffle.GetAllWorkflowApps(ctx, 100) - //allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - } - - workflow.PreviouslySaved = true - } - - //log.Printf("PRE TRIGGERS") - //workflow.Actions = newActions - - workflow.Actions = newActions - workflow.IsValid = true - log.Printf("[INFO] Tags: %#v", workflow.Tags) - - // FIXME: Is this too drastic? May lead to issues in the future. - // Should maybe make a copy for the old org. - if workflow.OrgId != user.ActiveOrg.Id { - log.Printf("[WARNING] Editing workflow to be owned by %s", user.ActiveOrg.Id) - workflow.OrgId = user.ActiveOrg.Id - workflow.ExecutingOrg = user.ActiveOrg - workflow.Org = append(workflow.Org, user.ActiveOrg) - } - - err = setWorkflow(ctx, workflow, fileId) - if err != nil { - log.Printf("Failed saving workflow to database: %s", err) - if workflow.PreviouslySaved { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - } - - totalOldActions := len(tmpworkflow.Actions) - totalNewActions := len(workflow.Actions) - err = increaseStatisticsField(ctx, "total_workflow_actions", workflow.ID, int64(totalNewActions-totalOldActions), workflow.OrgId) - if err != nil { - log.Printf("Failed to change total actions data: %s", err) - } - - type returnData struct { - Success bool `json:"success"` - Errors []string `json:"errors"` - } - - returndata := returnData{ - Success: true, - Errors: workflow.Errors, - } - - // Really don't know why this was happening - //cacheKey := fmt.Sprintf("workflowapps-sorted-100") - //requestCache.Delete(cacheKey) - //cacheKey = fmt.Sprintf("workflowapps-sorted-500") - //requestCache.Delete(cacheKey) - - log.Printf("[INFO] Saved new version of workflow %s (%s) for org %s", workflow.Name, fileId, workflow.OrgId) - resp.WriteHeader(200) - newBody, err := json.Marshal(returndata) - if err != nil { - resp.Write([]byte(`{"success": true}`)) - return - } - - resp.Write(newBody) -} func getWorkflowLocal(fileId string, request *http.Request) ([]byte, error) { fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s", localBase, fileId) @@ -3150,7 +2073,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { } } - err = setWorkflowExecution(ctx, *workflowExecution, true) + err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true) if err != nil { log.Printf("Error saving workflow execution for updates when aborting %s: %s", topic, err) resp.WriteHeader(401) @@ -3433,7 +2356,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request } oldExecution.Results = newResults - err = setWorkflowExecution(ctx, *oldExecution, true) + err = shuffle.SetWorkflowExecution(ctx, *oldExecution, true) if err != nil { log.Printf("Error saving workflow execution actionresult setting: %s", err) return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed setting workflowexecution actionresult in execution: %s", err), err @@ -3801,8 +2724,9 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request workflowExecution.Workflow.Org = []shuffle.Org{ workflowExecution.Workflow.ExecutingOrg, } + //Org []Org `json:"org,omitempty" datastore:"org"` - err = setWorkflowExecution(ctx, workflowExecution, true) + err = shuffle.SetWorkflowExecution(ctx, workflowExecution, true) if err != nil { log.Printf("Error saving workflow execution for updates %s: %s", topic, err) return shuffle.WorkflowExecution{}, "Failed getting workflowexecution", err @@ -4486,7 +3410,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { } workflow.Schedules = append(workflow.Schedules, schedule) - err = setWorkflow(ctx, *workflow, workflow.ID) + err = shuffle.SetWorkflow(ctx, *workflow, workflow.ID) if err != nil { log.Printf("Failed setting workflow for schedule: %s", err) resp.WriteHeader(401) @@ -4499,217 +3423,6 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { return } -// FIXME - add to actual database etc -func getSpecificWorkflow(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in getting specific workflow: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - - var fileId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[4] - } - - if strings.Contains(fileId, "?") { - fileId = strings.Split(fileId, "?")[0] - } - - if len(fileId) != 36 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Workflow ID when getting workflow is not valid"}`)) - return - } - - ctx := context.Background() - //memcacheName := fmt.Sprintf("%s_%s", user.Username, fileId) - //if item, err := memcache.Get(ctx, memcacheName); err == memcache.ErrCacheMiss { - // // Not in cache - // log.Printf("User %s not in cache.", memcacheName) - //} else if err != nil { - // log.Printf("Error getting item: %v", err) - //} else { - // log.Printf("Got workflow %s from cache", fileId) - // // FIXME - verify if value is ok? Can unmarshal etc. - // resp.WriteHeader(200) - // resp.Write(item.Value) - // return - //} - - workflow, err := shuffle.GetWorkflow(ctx, fileId) - if err != nil { - log.Printf("Workflow %s doesn't exist.", fileId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Item already exists."}`)) - return - } - - // CHECK orgs of user, or if user is owner - // FIXME - add org check too, and not just owner - // Check workflow.Sharing == private / public / org too - if user.Id != workflow.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for workflow %s (get workflow)", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if len(workflow.Actions) == 0 { - workflow.Actions = []shuffle.Action{} - } - if len(workflow.Branches) == 0 { - workflow.Branches = []shuffle.Branch{} - } - if len(workflow.Triggers) == 0 { - workflow.Triggers = []shuffle.Trigger{} - } - if len(workflow.Errors) == 0 { - workflow.Errors = []string{} - } - - // Only required for individuals I think - //newactions := []Action{} - //for _, item := range workflow.Actions { - // item.LargeImage = "" - // item.SmallImage = "" - // newactions = append(newactions, item) - //} - //workflow.Actions = newactions - - //newtriggers := []Trigger{} - //for _, item := range workflow.Triggers { - // item.LargeImage = "" - // newtriggers = append(newtriggers, item) - //} - //workflow.Triggers = newtriggers - - body, err := json.Marshal(workflow) - if err != nil { - log.Printf("Failed workflow GET marshalling: %s", err) - resp.WriteHeader(http.StatusInternalServerError) - resp.Write([]byte(`{"success": false}`)) - return - } - - //item := &memcache.Item{ - // Key: memcacheName, - // Value: body, - // Expiration: time.Minute * 60, - //} - //if err := memcache.Add(ctx, item); err == memcache.ErrNotStored { - // if err := memcache.Set(ctx, item); err != nil { - // log.Printf("Error setting item: %v", err) - // } - //} else if err != nil { - // log.Printf("error adding item: %v", err) - //} else { - // //log.Printf("Set cache for %s", item.Key) - //} - - resp.WriteHeader(200) - resp.Write(body) -} - -func setWorkflowExecution(ctx context.Context, workflowExecution shuffle.WorkflowExecution, dbSave bool) error { - //log.Printf("\n\n\nRESULT: %s\n\n\n", workflowExecution.Status) - if len(workflowExecution.ExecutionId) == 0 { - log.Printf("Workflowexeciton executionId can't be empty.") - return errors.New("ExecutionId can't be empty.") - } - - cacheKey := fmt.Sprintf("workflowexecution-%s", workflowExecution.ExecutionId) - requestCache.Set(cacheKey, &workflowExecution, cache.DefaultExpiration) - if !dbSave && workflowExecution.Status == "EXECUTING" && len(workflowExecution.Results) > 1 { - //log.Printf("[WARNING] SHOULD skip DB saving for execution") - return nil - } - - // New struct, to not add body, author etc - key := datastore.NameKey("workflowexecution", workflowExecution.ExecutionId, nil) - if _, err := dbclient.Put(ctx, key, &workflowExecution); err != nil { - log.Printf("Error adding workflow_execution: %s", err) - return err - } - - return nil -} - -func getWorkflowExecution(ctx context.Context, id string) (*shuffle.WorkflowExecution, error) { - workflowExecution := &shuffle.WorkflowExecution{} - cacheKey := fmt.Sprintf("workflowexecution-%s", id) - if value, found := requestCache.Get(cacheKey); found { - parsedValue := value.(*shuffle.WorkflowExecution) - //log.Printf("Found execution for id %s with %d results", parsedValue.ExecutionId, len(parsedValue.Results)) - return parsedValue, nil - - //log.Printf("[INFO] FOUND key %s with value length %d", cacheKey, len(parsedValue)) - //err := json.Unmarshal([]byte(parsedValue), &workflowExecution) - //if err == nil { - // log.Printf("SHOULD RETURN CACHED EXECUTION of length %d", len(parsedValue)) - //} else { - // log.Printf("Failed unmarshalling cached value: %s", err) - //} - } else { - //log.Printf("[ERROR] Couldn't find key %s", cacheKey) - } - - key := datastore.NameKey("workflowexecution", strings.ToLower(id), nil) - if err := dbclient.Get(ctx, key, workflowExecution); err != nil { - return &shuffle.WorkflowExecution{}, err - } - - return workflowExecution, nil -} - -//func shuffle.GetApp(ctx context.Context, id string) (*WorkflowApp, error) { -// key := datastore.NameKey("workflowapp", strings.ToLower(id), nil) -// workflowApp := &WorkflowApp{} -// if err := dbclient.Get(ctx, key, workflowApp); err != nil { -// return &WorkflowApp{}, err -// -// } -// -// return workflowApp, nil -//} -// -//func shuffle.GetWorkflow(ctx context.Context, id string) (*shuffle.Workflow, error) { -// key := datastore.NameKey("workflow", strings.ToLower(id), nil) -// workflow := &Workflow{} -// if err := dbclient.Get(ctx, key, workflow); err != nil { -// return &Workflow{}, err -// } -// -// return workflow, nil -//} -// -//func shuffle.GetEnvironments(ctx context.Context, orgId string) ([]Environment, error) { -// var environments []Environment -// q := datastore.NewQuery("Environments").Filter("org_id =", orgId) -// -// _, err := dbclient.GetAll(ctx, q, &environments) -// if err != nil { -// return []Environment{}, err -// } -// -// return environments, nil -//} - func setExampleresult(ctx context.Context, result shuffle.AppExecutionExample) error { // FIXME: Reintroduce this for stats //key := datastore.NameKey("example_result", result.ExampleId, nil) @@ -4723,78 +3436,6 @@ func setExampleresult(ctx context.Context, result shuffle.AppExecutionExample) e return nil } -// Hmm, so I guess this should use uuid :( -// Consistency PLX -func setWorkflow(ctx context.Context, workflow shuffle.Workflow, id string, optionalEditedSecondsOffset ...int) error { - workflow.Edited = int64(time.Now().Unix()) - if len(optionalEditedSecondsOffset) > 0 { - workflow.Edited += int64(optionalEditedSecondsOffset[0]) - } - - key := datastore.NameKey("workflow", id, nil) - - // New struct, to not add body, author etc - if _, err := dbclient.Put(ctx, key, &workflow); err != nil { - log.Printf("Error adding workflow: %s", err) - return err - } - - return nil -} - -func deleteAppAuthentication(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, userErr := shuffle.HandleApiAuthentication(resp, request) - if userErr != nil { - log.Printf("Api authentication failed in edit workflow: %s", userErr) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if user.Role != "admin" { - log.Printf("Need to be admin to delete appauth") - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - log.Printf("%#v", location) - var fileId string - if location[1] == "api" { - if len(location) <= 5 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[5] - } - - // FIXME: Set affected workflows to have errors - // 1. Get the auth - // 2. Loop the workflows (.Usage) and set them to have errors - // 3. Loop the nodes in workflows and do the same - - log.Printf("ID: %s", fileId) - ctx := context.Background() - err := DeleteKey(ctx, "workflowappauth", fileId) - if err != nil { - log.Printf("Failed deleting workflowapp") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed deleting workflow app"}`))) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - // FIXME: Not suitable for cloud right now :O func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) @@ -4896,7 +3537,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { //} } - err = setWorkflow(ctx, workflow, workflow.ID) + err = shuffle.SetWorkflow(ctx, workflow, workflow.ID) if err != nil { log.Printf("Failed setting workflow when deleting app: %s", err) continue @@ -4948,9 +3589,9 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { log.Printf("Failed to increase total apps loaded stats: %s", err) } cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) //err = memcache.Delete(request.Context(), sessionToken) resp.WriteHeader(200) @@ -5064,416 +3705,6 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { resp.Write(data) } -func setAuthenticationConfig(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, userErr := shuffle.HandleApiAuthentication(resp, request) - if userErr != nil { - log.Printf("Api authentication failed in get all apps: %s", userErr) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if user.Role != "admin" { - log.Printf("[WARNING] User isn't admin during auth edit config") - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Must be admin to perform this action"}`))) - return - } - - var fileId string - location := strings.Split(request.URL.String(), "/") - if location[1] == "api" { - if len(location) <= 5 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - fileId = location[5] - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Error with body read: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - type configAuth struct { - Id string `json:"id"` - Action string `json:"action"` - } - - var config configAuth - err = json.Unmarshal(body, &config) - if err != nil { - log.Printf("Failed unmarshaling (appauth): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if config.Id != fileId { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Bad ID match"}`)) - return - } - - ctx := context.Background() - auth, err := shuffle.GetWorkflowAppAuthDatastore(ctx, fileId) - if err != nil { - log.Printf("Authget error: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ":("}`)) - return - } - - if auth.OrgId != user.ActiveOrg.Id { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "User can't edit this org"}`)) - return - } - - if config.Action == "assign_everywhere" { - log.Printf("Should set authentication config") - q := datastore.NewQuery("workflow").Filter("org_id =", user.ActiveOrg.Id) - q = q.Order("-edited").Limit(35) - - var workflows []shuffle.Workflow - _, err = dbclient.GetAll(ctx, q, &workflows) - if err != nil { - log.Printf("Getall error in auth update: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed getting workflows to update"}`)) - return - } - - // FIXME: Add function to remove auth from other auth's - actionCnt := 0 - workflowCnt := 0 - authenticationUsage := []shuffle.AuthenticationUsage{} - for _, workflow := range workflows { - newActions := []shuffle.Action{} - edited := false - usage := shuffle.AuthenticationUsage{ - WorkflowId: workflow.ID, - Nodes: []string{}, - } - - for _, action := range workflow.Actions { - if action.AppName == auth.App.Name { - action.AuthenticationId = auth.Id - - edited = true - actionCnt += 1 - usage.Nodes = append(usage.Nodes, action.ID) - } - - newActions = append(newActions, action) - } - - workflow.Actions = newActions - if edited { - //auth.Usage = usage - authenticationUsage = append(authenticationUsage, usage) - err = setWorkflow(ctx, workflow, workflow.ID) - if err != nil { - log.Printf("Failed setting (authupdate) workflow: %s", err) - continue - } - - workflowCnt += 1 - } - } - - //Usage []AuthenticationUsage `json:"usage" datastore:"usage"` - log.Printf("[INFO] Found %d workflows, %d actions", workflowCnt, actionCnt) - if actionCnt > 0 && workflowCnt > 0 { - auth.WorkflowCount = int64(workflowCnt) - auth.NodeCount = int64(actionCnt) - auth.Usage = authenticationUsage - auth.Defined = true - - err = shuffle.SetWorkflowAppAuthDatastore(ctx, *auth, auth.Id) - if err != nil { - log.Printf("Failed setting appauth: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed setting app auth for all workflows"}`)) - return - } else { - // FIXME: Remove ALL workflows from other auths using the same - } - } - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) - //var config configAuth - - //log.Printf("Should set %s -} - -func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, userErr := shuffle.HandleApiAuthentication(resp, request) - if userErr != nil { - log.Printf("Api authentication failed in get all apps: %s", userErr) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Error with body read: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - var appAuth shuffle.AppAuthenticationStorage - err = json.Unmarshal(body, &appAuth) - if err != nil { - log.Printf("Failed unmarshaling (appauth): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - ctx := context.Background() - if len(appAuth.Id) == 0 { - appAuth.Id = uuid.NewV4().String() - } else { - auth, err := shuffle.GetWorkflowAppAuthDatastore(ctx, appAuth.Id) - if err == nil { - // OrgId string `json:"org_id" datastore:"org_id"` - if auth.OrgId != user.ActiveOrg.Id { - log.Printf("[WARNING] User isn't a part of the right org during auth edit") - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": ":("}`))) - return - } - - if user.Role != "admin" { - log.Printf("[WARNING] User isn't admin during auth edit") - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": ":("}`))) - return - } - - if !auth.Active { - log.Printf("[WARNING] Auth isn't active for edit") - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't update an inactive auth"}`))) - return - } - - if auth.App.Name != appAuth.App.Name { - log.Printf("[WARNING] User tried to modify auth") - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad app configuration: need to specify correct name"}`))) - return - } - } - } - - if len(appAuth.Label) == 0 { - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Label can't be empty"}`))) - return - } - - // Super basic check - if len(appAuth.App.ID) != 36 && len(appAuth.App.ID) != 32 { - log.Printf("Bad ID for app: %s", appAuth.App.ID) - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "App has to be defined"}`))) - return - } - - // FIXME: Doens't validate Org - app, err := shuffle.GetApp(ctx, appAuth.App.ID) - if err != nil { - log.Printf("[WARNING] Failed finding app %s while setting auth. Finding it by looping apps.", appAuth.App.ID) - workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 500) - if err != nil { - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - foundIndex := -1 - for i, workflowapp := range workflowapps { - if workflowapp.Name == appAuth.App.Name { - foundIndex = i - break - } - } - - if foundIndex >= 0 { - log.Printf("[INFO] Found app %s by looping auth", appAuth.App.ID) - } else { - log.Printf("[ERROR] Failed finding app %s which has auth after looping", appAuth.App.ID) - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - } - - // Check if the items are correct - for _, field := range appAuth.Fields { - found := false - for _, param := range app.Authentication.Parameters { - if field.Key == param.Name { - found = true - } - } - - if !found { - log.Printf("Failed finding field %s in appauth fields", field.Key) - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "All auth fields required"}`))) - return - } - } - - //appAuth.LargeImage = "" - appAuth.OrgId = user.ActiveOrg.Id - appAuth.Defined = true - err = shuffle.SetWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) - if err != nil { - log.Printf("Failed setting up app auth %s: %s", appAuth.Id, err) - resp.WriteHeader(409) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func getAppAuthentication(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, userErr := shuffle.HandleApiAuthentication(resp, request) - if userErr != nil { - log.Printf("Api authentication failed in get all apps: %s", userErr) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME: Auth to get the right ones only - //if user.Role != "admin" { - // log.Printf("User isn't admin") - // resp.WriteHeader(401) - // resp.Write([]byte(`{"success": false}`)) - // return - //} - ctx := context.Background() - allAuths, err := shuffle.GetAllWorkflowAppAuth(ctx, user.ActiveOrg.Id) - if err != nil { - log.Printf("Api authentication failed in get all app auth: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if len(allAuths) == 0 { - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true, "data": []}`)) - return - } - - // Cleanup for frontend usage. User shouldn't be able to get the data. - newAuth := []shuffle.AppAuthenticationStorage{} - for _, auth := range allAuths { - newAuthField := auth - for index, _ := range auth.Fields { - newAuthField.Fields[index].Value = "auth placeholder (replaced during execution)" - } - - newAuth = append(newAuth, newAuthField) - } - - newbody, err := json.Marshal(allAuths) - if err != nil { - log.Printf("Failed unmarshalling all app auths: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unpacking workflow app auth"}`))) - return - } - - data := fmt.Sprintf(`{"success": true, "data": %s}`, string(newbody)) - - resp.WriteHeader(200) - resp.Write([]byte(data)) - - /* - data := `{ - "success": true, - "data": [ - { - "app": { - "name": "thehive", - "description": "what", - "app_version": "1.0.0", - "id": "4f97da9d-1caf-41cc-aa13-67104d8d825c", - "large_image": "asd" - }, - "fields": { - "apikey": "hello", - "url": "url" - }, - "usage": [{ - "workflow_id": "asd", - "nodes": [{ - "node_id": "" - }] - }], - "label": "Original", - "id": "4f97da9d-1caf-41cc-aa13-67104d8d825d", - "active": true - }, - { - "app": { - "name": "thehive", - "description": "what", - "app_version": "1.0.0", - "id": "4f97da9d-1caf-41cc-aa13-67104d8d825c", - "large_image": "asd" - }, - "fields": { - "apikey": "hello", - "url": "url" - }, - "usage": [{ - "workflow_id": "asd", - "nodes": [{ - "node_id": "" - }] - }], - "label": "Number 2", - "id": "4f97da9d-1caf-41cc-aa13-67104d8d825d", - "active": true - } - ] - }` - */ -} func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -5555,9 +3786,9 @@ func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) { } cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) log.Printf("Changed workflow app %s", app.ID) resp.WriteHeader(200) @@ -6169,10 +4400,11 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { return } + ctx := context.Background() cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) // Just need to be logged in // FIXME - should have some permissions? @@ -6198,7 +4430,7 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { } log.Printf("[INFO] Starting hotloading from %s", location) - err = handleAppHotload(location, true) + err = handleAppHotload(ctx, location, true) if err != nil { log.Printf("Failed app hotload: %s", err) resp.WriteHeader(500) @@ -6207,9 +4439,9 @@ func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) { } cacheKey = fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) @@ -6337,10 +4569,11 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) { return } + ctx := context.Background() cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) @@ -6477,9 +4710,9 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, } cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) } } else { //log.Printf("Skipped upload of %s (%s)", api.Name, api.ID) @@ -6593,7 +4826,7 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra log.Printf("Import workflow from file: %s", filename) ctx := context.Background() - err = setWorkflow(ctx, workflow, workflow.ID, secondsOffset) + err = shuffle.SetWorkflow(ctx, workflow, workflow.ID, secondsOffset) if err != nil { log.Printf("Failed setting (download) workflow: %s", err) continue @@ -6896,9 +5129,9 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin // This is getting silly cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) //log.Printf("BUILDLATERFIRST: %d, BUILDLATERLIST: %d", len(buildLaterFirst), len(buildLaterList)) if len(extra) == 0 { @@ -7020,11 +5253,10 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { log.Printf("Added %s:%s to the database", workflowapp.Name, workflowapp.AppVersion) } - //memcache.Delete(ctx, "all_apps") cacheKey := fmt.Sprintf("workflowapps-sorted-100") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) cacheKey = fmt.Sprintf("workflowapps-sorted-500") - requestCache.Delete(cacheKey) + shuffle.DeleteCache(ctx, cacheKey) resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 4cd4939a..51d09de6 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -173,7 +173,6 @@ const AngularWorkflow = (props) => { const [selectedApp, setSelectedApp] = React.useState({}); const [selectedAction, setSelectedAction] = React.useState({}); - const [selectedActionName, setSelectedActionName] = React.useState({}); const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({}); const [executionRequest, setExecutionRequest] = React.useState({}) @@ -1173,20 +1172,25 @@ const AngularWorkflow = (props) => { const onNodeSelect = (event, newAppAuth) => { const data = event.target.data() setLastSaved(false) - const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id) + //const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id) console.log("NODE: ", data) - console.log("BRANCHES: ", branch) + //console.log("BRANCHES: ", branch) if (data.type === "ACTION") { + + // FIXME - unselect //console.log(cy.elements('[_id!="${data._id}"]`)) // Does it choose the wrong action? var curaction = workflow.actions.find(a => a.id === data.id) if (!curaction || curaction === undefined) { + //event.target.unselect() //alert.error("Action not found. Please remake it.") return } + setSelectedAction(curaction) + const curapp = apps.find(a => a.name === curaction.app_name && a.app_version === curaction.app_version) if (!curapp || curapp === undefined) { alert.error("App "+curaction.app_name+" not found. Did someone delete it?") @@ -1202,7 +1206,7 @@ const AngularWorkflow = (props) => { } var tmpAuth = JSON.parse(JSON.stringify(newAppAuth)) - console.log("Checking authentication: ", tmpAuth) + //console.log("Checking authentication: ", tmpAuth) for (var key in tmpAuth) { var item = tmpAuth[key] @@ -1221,7 +1225,7 @@ const AngularWorkflow = (props) => { } curaction.authentication = authenticationOptions - console.log("Authentication: ", authenticationOptions) + //console.log("Authentication: ", authenticationOptions) if (curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "") { curaction.selectedAuthentication = {} } @@ -1243,8 +1247,6 @@ const AngularWorkflow = (props) => { setSelectedActionEnvironment(env) } - setSelectedActionName(curaction.name) - setSelectedAction(curaction) /* var params = [] @@ -1273,7 +1275,6 @@ const AngularWorkflow = (props) => { const trigger_index = workflow.triggers.findIndex(a => a.id === data.id) setSelectedTriggerIndex(trigger_index) setSelectedTrigger(data) - setSelectedActionName(data.name) setSelectedActionEnvironment(data.env) if (data.app_name === "Shuffle Workflow") { @@ -1781,7 +1782,6 @@ const AngularWorkflow = (props) => { const removeNode = () => { setSelectedApp({}) setSelectedAction({}) - setSelectedActionName("") const selectedNode = cy.$(':selected') if (selectedNode.data() === undefined) { @@ -2757,7 +2757,6 @@ const AngularWorkflow = (props) => { //setSelectedActionEnvironment(env) setSelectedAction(selectedAction) - setSelectedActionName(e.target.value) } // APPSELECT at top @@ -2766,7 +2765,7 @@ const AngularWorkflow = (props) => { // ACTION select // const selectedNameChange = (event) => { - console.log("OLDNAME: ", selectedActionName) + //console.log("OLDNAME: ", selectedAction.name) event.target.value = event.target.value.replace("(", "") event.target.value = event.target.value.replace(")", "") event.target.value = event.target.value.replace("$", "") @@ -2777,7 +2776,7 @@ const AngularWorkflow = (props) => { selectedAction.label = event.target.value setSelectedAction(selectedAction) - console.log("SHOULD CHANGE NAME EVERYWHERE ITS USED TOO BASED ON OLD NAME!") + //console.log("SHOULD CHANGE NAME EVERYWHERE ITS USED TOO BASED ON OLD NAME!") /* if (nodeaction.label !== curaction.label) { @@ -3103,7 +3102,6 @@ const AngularWorkflow = (props) => { selectedActionParameters[count].action_field = fieldvalue selectedAction.parameters = selectedActionParameters - setSelectedActionName(selectedActionName) setSelectedApp(selectedApp) setSelectedAction(selectedAction) setUpdate(fieldvalue) @@ -3124,7 +3122,6 @@ const AngularWorkflow = (props) => { // FIXME - check if startnode // Set value - setSelectedActionName(selectedActionName) setSelectedApp(selectedApp) setSelectedAction(selectedAction) @@ -3151,7 +3148,6 @@ const AngularWorkflow = (props) => { selectedAction.parameters = selectedActionParameters // This is a stupid workaround to make it refresh rofl - setSelectedActionName({}) setSelectedAction({}) setSelectedTrigger({}) setSelectedApp({}) @@ -3159,7 +3155,6 @@ const AngularWorkflow = (props) => { // FIXME - check if startnode // Set value - setSelectedActionName(selectedActionName) setSelectedApp(selectedApp) setSelectedAction(selectedAction) } @@ -4199,7 +4194,7 @@ const AngularWorkflow = (props) => { Actions
    } + > + + Admin + + + User + + + } style ={{ minWidth: 135, maxWidth: 135, marginRight: 15,}} /> { - {errorCode.length > 0 ? `Error: ${errorCode}` : null} + + {errorCode.length > 0 ? `Error: ${errorCode}` : null} +
    diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 0b4cd611..b7bc7626 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -195,6 +195,10 @@ const Apps = (props) => { setIsLoading(false) if (response.status !== 200) { console.log("Status not 200 for apps :O!") + + if (isCloud) { + window.location.pathname = "/search" + } } return response.json() @@ -648,11 +652,17 @@ const Apps = (props) => { upload = ref} onChange={importFiles} /> {workflows.length > 0 ? From 1b428b301a8177b6fc7a150850a1fb9113c220a0 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 29 Mar 2021 21:58:48 +0200 Subject: [PATCH 180/185] Fixed no-path issue with requirement of leading / --- backend/Dockerfile | 3 - backend/go-app/go.mod | 4 +- backend/go-app/main.go | 139 ++++++++++++++++- backend/go-app/walkoff.go | 3 +- docker-compose.yml | 12 +- frontend/Dockerfile | 2 +- frontend/src/defaultCytoscapeStyle.js | 21 ++- frontend/src/views/AngularWorkflow.jsx | 125 ++++++++++++--- frontend/src/views/AppCreator.jsx | 6 +- frontend/src/views/MyView.jsx | 205 +++++++++++++------------ frontend/src/views/Workflows.jsx | 24 +-- functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/orborus.go | 2 +- functions/onprem/worker/Dockerfile | 5 +- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/worker.go | 25 +-- 16 files changed, 419 insertions(+), 161 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index ea93625e..5d7c484d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,15 +7,12 @@ WORKDIR /app ADD ./go-app/main.go /app ADD ./go-app/walkoff.go /app ADD ./go-app/docker.go /app -ADD ./go-app/codegen.go /app -ADD ./go-app/files.go /app ADD ./go-app/oauth2.go /app ADD ./go-app/go.mod /app # Required files for code generation ADD ./app_sdk/app_base.py /app_sdk -ADD ./app_sdk/static_baseline.py /app_sdk ADD ./app_sdk_kali/app_base.py /app_sdk_kali ADD ./app_sdk_kali/static_baseline.py /app_sdk_kali ADD ./app_sdk_blackarch/app_base.py /app_sdk_blackarch diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 7d7f91f2..fa5a2e35 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,7 +2,7 @@ module shuffle go 1.13 -replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared +//replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi require ( @@ -18,7 +18,7 @@ require ( github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect - github.com/frikky/shuffle-shared v0.0.15 + github.com/frikky/shuffle-shared v0.0.20 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0 diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 7d1de1b2..097bc190 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -77,6 +77,8 @@ var registryName = "registry.hub.docker.com" var runningEnvironment = "onprem" var syncUrl = "https://shuffler.io" + +//var syncUrl = "http://localhost:5002" var syncSubUrl = "https://shuffler.io" //var syncUrl = "http://localhost:5002" @@ -5463,12 +5465,12 @@ func runInit(ctx context.Context) { } iterateOpenApiGithub(fs, dir, "", "") - log.Printf("Finished downloading extra API samples") + log.Printf("[INFO] Finished downloading extra API samples") } workflowLocation := os.Getenv("SHUFFLE_DOWNLOAD_WORKFLOW_LOCATION") if len(workflowLocation) > 0 { - log.Printf("Downloading WORKFLOWS from %s if no workflows - EXTRA workflows", workflowLocation) + log.Printf("[INFO] Downloading WORKFLOWS from %s if no workflows - EXTRA workflows", workflowLocation) q := datastore.NewQuery("workflow").Limit(35) var workflows []shuffle.Workflow _, err = dbclient.GetAll(ctx, q, &workflows) @@ -5918,6 +5920,138 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { resp.Write(respBody) } +func makeWorkflowPublic(resp http.ResponseWriter, request *http.Request) { + cors := shuffle.HandleCors(resp, request) + if cors { + return + } + + user, userErr := shuffle.HandleApiAuthentication(resp, request) + if userErr != nil { + log.Printf("[WARNING] Api authentication failed in make workflow public: %s", userErr) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + location := strings.Split(request.URL.String(), "/") + var fileId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + ctx := context.Background() + if strings.Contains(fileId, "?") { + fileId = strings.Split(fileId, "?")[0] + } + + if len(fileId) != 36 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Workflow ID when getting workflow is not valid"}`)) + return + } + + workflow, err := shuffle.GetWorkflow(ctx, fileId) + if err != nil { + log.Printf("[WARNING] Workflow %s doesn't exist in app publish. User: %s", fileId, user.Id) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // CHECK orgs of user, or if user is owner + // FIXME - add org check too, and not just owner + // Check workflow.Sharing == private / public / org too + if user.Id != workflow.Owner || len(user.Id) == 0 { + if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" { + log.Printf("[INFO] User %s is accessing workflow %s as admin", user.Username, workflow.ID) + } else { + log.Printf("[WARNING] Wrong user (%s) for workflow %s (get workflow)", user.Username, workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + } + + if !workflow.IsValid || !workflow.PreviouslySaved { + log.Printf("[INFO] Failed uploading workflow because it's invalid or not saved") + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Invalid workflows are not sharable"}`)) + return + } + + // Starting validation of the POST workflow + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("[WARNING] Body data error on mail: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + parsedWorkflow := shuffle.Workflow{} + err = json.Unmarshal(body, &parsedWorkflow) + if err != nil { + log.Printf("[WARNING] Unmarshal error on mail: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // Super basic validation. Doesn't really matter. + if parsedWorkflow.ID != workflow.ID || len(parsedWorkflow.Actions) != len(workflow.Actions) { + log.Printf("[WARNING] Bad ID during publish: %s vs %s", workflow.ID, parsedWorkflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if !workflow.IsValid || !workflow.PreviouslySaved { + log.Printf("[INFO] Failed uploading new workflow because it's invalid or not saved") + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Invalid workflows are not sharable"}`)) + return + } + + workflowData, err := json.Marshal(parsedWorkflow) + if err != nil { + log.Printf("[WARNING] Failed marshalling workflow: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // Sanitization is done in the frontend as well + parsedWorkflow = shuffle.SanitizeWorkflow(parsedWorkflow) + parsedWorkflow.ID = uuid.NewV4().String() + action := shuffle.CloudSyncJob{ + Type: "workflow", + Action: "publish", + OrgId: user.ActiveOrg.Id, + PrimaryItemId: workflow.ID, + SecondaryItem: string(workflowData), + FifthItem: user.Id, + } + + err = executeCloudAction(action, user.ActiveOrg.SyncConfig.Apikey) + if err != nil { + log.Printf("[WARNING] Failed cloud PUBLISH: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + + log.Printf("[INFO] Successfully published workflow %s (%s) TO CLOUD", workflow.Name, workflow.ID) + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) +} + func initHandlers() { var err error ctx := context.Background() @@ -6041,6 +6175,7 @@ func initHandlers() { r.HandleFunc("/api/v1/get_openapi/{key}", getOpenapi).Methods("GET", "OPTIONS") // NEW for 0.8.0 + r.HandleFunc("/api/v1/workflows/{key}/publish", makeWorkflowPublic).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 3ff9bdf2..86f27be7 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -981,7 +981,7 @@ func validateNewWorkerExecution(body []byte) error { execution.Status = "FINISHED" } - log.Printf("BASEEXECUTION LENGTH: %d", len(baseExecution.Workflow.Actions)+extra) + log.Printf("[INFO] BASEEXECUTION LENGTH: %d", len(baseExecution.Workflow.Actions)+extra) } // FIXME: Add extra here @@ -4782,6 +4782,7 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin workflowapp.Sharing = true workflowapp.Downloaded = true workflowapp.Hash = md5 + workflowapp.Public = true err = shuffle.SetWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) if err != nil { diff --git a/docker-compose.yml b/docker-compose.yml index b96b2441..45656376 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: '3' services: frontend: - #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.64 + build: ./frontend + image: ghcr.io/frikky/shuffle-frontend:0.8.70 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -16,8 +16,8 @@ services: depends_on: - backend backend: - #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.64 + build: ./backend + image: ghcr.io/frikky/shuffle-backend:0.8.70 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -47,7 +47,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.63 + image: ghcr.io/frikky/shuffle-orborus:0.8.70 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -56,7 +56,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - SHUFFLE_APP_SDK_VERSION=0.8.60 - - SHUFFLE_WORKER_VERSION=0.8.63 + - SHUFFLE_WORKER_VERSION=0.8.70 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cd11e347..4204651c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -21,7 +21,7 @@ COPY ./*.json /usr/src/app/ RUN yarn build # Production environment -FROM nginx:latest +FROM nginx:1.19 RUN mkdir -p /usr/share/nginx/html/build RUN mkdir -p /usr/share/nginx/html/css diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index ff9990a9..f2538a94 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -39,6 +39,23 @@ const data = [{ 'border-color': '#81c784', 'background-width': '100%', 'background-height': '100%', + 'border-radius': '5px', + }, + }, + { + selector: `node[app_name="Shuffle Tools"]`, + css: { + 'width': '30px', + 'height': '30px', + 'font-size': '0px', + }, + }, + { + selector: `node[app_name="Testing"]`, + css: { + 'width': '30px', + 'height': '30px', + 'font-size': '0px', }, }, { @@ -112,7 +129,7 @@ const data = [{ }, }, { - selector: 'node:selected', + selector: ':selected', css: { 'background-color': '#77b0d0', 'border-color': '#77b0d0', @@ -187,6 +204,8 @@ const data = [{ 'border-width': '12px', 'transition-property': 'border-width', 'transition-duration': '0.25s', + 'font-size': '30px', + 'label': 'data(label)', }, }, { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index dbe016f6..8082415d 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -92,7 +92,6 @@ const AngularWorkflow = (props) => { const theme = useTheme(); const [bodyWidth, bodyHeight] = useWindowSize(); - const appBarSize = 75 var to_be_copied = "" const [cystyle, ] = useState(cytoscapestyle) @@ -190,18 +189,22 @@ const AngularWorkflow = (props) => { const [workflowExecutions, setWorkflowExecutions] = React.useState([]); const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0) + // This should all be set once, not on every iteration + // Use states and don't update lol const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false //const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" + const appBarSize = isCloud ? 75 : 60 const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"] - const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?' + useBeforeunload(() => { if (!lastSaved) { return unloadText } }) + const [elements, setElements] = useState([]) // No point going as fast, as the nodes aren't realtime anymore, but bulk updated. // Set it from 2500 to 6000 to reduce overall load @@ -1121,7 +1124,7 @@ const AngularWorkflow = (props) => { } if (responseJson.public) { - alert.info("This workflow is public. You'll have to save it to make it your own!") + alert.info("This workflow is public. You will have to save it to make it your own!") setLastSaved(false) } @@ -1215,9 +1218,36 @@ const AngularWorkflow = (props) => { setSelectedTrigger({}) } + // Comparing locations between nodes and setting views + const onNodeDrag = (event, newAppAuth) => { + //console.log("DRAGGING: ", event.target) + //console.log("LEN2: ", event.target.edges.length) + + + /* + event.target.animate({ + style: { + "border-width": "12px", + "border-opacity": ".7", + } + }, { + duration: animationDuration, + }) + event.target.animate({ + style: { + "border-width": "12px", + "border-opacity": ".7", + } + }, { + duration: animationDuration, + }) + */ + } + // Nodeselectbatching: // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once const onNodeSelect = (event, newAppAuth) => { + const data = event.target.data() setLastSaved(false) @@ -1619,6 +1649,17 @@ const AngularWorkflow = (props) => { }); } + + if (!firstrequest && graphSetup && established && props.match.params.key !== workflow.id && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0) { + //console.log(props.match.params.key, workflow.id) + //getWorkflow() + //setCy() + //getWorkflowExecution(props.match.params.key, "") + //setEstablished(false) + //setGraphSetup(false) + window.location.pathname = "/workflows/"+props.match.params.key + } + useEffect(() => { if (firstrequest) { setFirstrequest(false) @@ -1642,14 +1683,14 @@ const AngularWorkflow = (props) => { } // App length necessary cus of cy initialization - if (elements.length === 0 && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) { + if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) { setGraphSetup(true) setupGraph() } else if (!established && cy !== undefined && apps !== null && apps !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0 && authLoaded){ //This part has to load LAST, as it's kind of not async. //This means we need everything else to happen first. - console.log("AUTH IN HERE: ", appAuthentication) - + // + //console.log("IN THIS PART AGAIN") setEstablished(true) cy.edgehandles({ @@ -1677,6 +1718,9 @@ const AngularWorkflow = (props) => { cy.on('mouseover', 'node', (e) => onNodeHover(e)) cy.on('mouseout', 'node', (e) => onNodeHoverOut(e)) + // Handles dragging + //cy.on('drag', 'node', (e) => onNodeDrag(e)) + //cy.on('mouseover', 'node', () => $(targetElement).addClass('mouseover')); //cy.on('cxttapstart', 'node', (e) => edgeHandler.start(e.target)) @@ -1711,7 +1755,7 @@ const AngularWorkflow = (props) => { const onNodeHover = (event) => { event.target.animate({ style: { - "border-width": "5px", + "border-width": "7px", "border-opacity": ".7", } }, { @@ -1727,17 +1771,23 @@ const AngularWorkflow = (props) => { // This is here to have a proper transition for lines const onEdgeHover = (event) => { + if (event === null || event === undefined) { + return + } + const sourcecolor = cy.getElementById(event.target.data("source")).style("border-color") const targetcolor = cy.getElementById(event.target.data("target")).style("border-color") - event.target.animate({ - style: { - "line-fill": "linear-gradient", - 'target-arrow-color': targetcolor, - "line-gradient-stop-colors": [sourcecolor, targetcolor], - "line-gradient-stop-positions": [0, 1], - }, - duration: 0, - }) + if (sourcecolor !== null && sourcecolor !== undefined && targetcolor !== null && targetcolor !== undefined) { + event.target.animate({ + style: { + "line-fill": "linear-gradient", + 'target-arrow-color': targetcolor, + "line-gradient-stop-colors": [sourcecolor, targetcolor], + "line-gradient-stop-positions": [0, 1], + }, + duration: 0, + }) + } } @@ -1799,6 +1849,31 @@ const AngularWorkflow = (props) => { conditions: conditions, hasErrors: branch.has_errors }; + + // This is an attempt at prettier edges. The numbers are weird to work with. + /* + //http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html + const sourcenode = actions.find(node => node.data._id === branch.source_id) + const destinationnode = actions.find(node => node.data._id === branch.destination_id) + if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) { + //node.data._id = action["id"] + console.log("SOURCE: ", sourcenode.position) + console.log("DESTINATIONNODE: ", destinationnode.position) + + var opposite = true + if (sourcenode.position.x > destinationnode.position.x) { + opposite = false + } else { + opposite = true + } + + edge.style = { + 'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"], + 'control-point-weight': ['0.3', '0.7'], + } + } + */ + return edge; }) @@ -4506,7 +4581,7 @@ const AngularWorkflow = (props) => { : null } onClick={() => { - console.log("CHANGE FIELD") + //console.log("CHANGE FIELD") }} onBlur={(e) => { changeActionVariable(data.action_field, e.target.value) @@ -4803,6 +4878,7 @@ const AngularWorkflow = (props) => { onClick={() => { setSelectedEdge({}) + var data = { condition: conditionValue, source: sourceValue, @@ -4821,6 +4897,18 @@ const AngularWorkflow = (props) => { } } + var label = "" + if (selectedEdge.conditions.length === 1) { + label = selectedEdge.conditions.length+" condition" + } else if (selectedEdge.conditions.length > 1) { + label = selectedEdge.conditions.length+" conditions" + } + + var currentedge = cy.getElementById(selectedEdge.id) + if (currentedge !== undefined && currentedge !== null) { + currentedge.data().label = label + } + setSelectedEdge(selectedEdge) workflow.branches[selectedEdgeIndex] = selectedEdge setWorkflow(workflow) @@ -6494,7 +6582,7 @@ const AngularWorkflow = (props) => { {/* */} - + {workflow.configuration !== null && workflow.configuration !== undefined && workflow.configuration.exit_on_error !== undefined ? : null}
    ) @@ -7211,6 +7299,7 @@ const AngularWorkflow = (props) => { stylesheet={cystyle} boxSelectionEnabled={true} autounselectify={false} + showGrid={true} cy={(incy) => { // FIXME: There's something specific loading when // you do the first hover of a node. Why is this different? diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 483949be..38f40744 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1565,9 +1565,9 @@ const AppCreator = (props) => { } // Url verification - if (currentAction.url.length === 0) { - errormessage.push("URL path can't be empty.") - } else if (!currentAction.url.startsWith("/") && baseUrl.length > 0) { + //if (currentAction.url.length === 0) { + // errormessage.push("URL path can't be empty.") + if (!currentAction.url.startsWith("/") && baseUrl.length > 0 && currentAction.url.length > 0) { errormessage.push("URL must start with /") } diff --git a/frontend/src/views/MyView.jsx b/frontend/src/views/MyView.jsx index 6ca5d0b0..b456c6ed 100644 --- a/frontend/src/views/MyView.jsx +++ b/frontend/src/views/MyView.jsx @@ -68,11 +68,9 @@ const flexContainerStyle = { } const flexBoxStyle = { - width: "333px", - height: "125px", - margin: "10px", - borderRadius: "4px", - boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.16), 0px 2px 4px rgba(0, 0, 0, 0.12), 0px 1px 8px rgba(0, 0, 0, 0.1)", + width: 333, + height: 125, + borderRadius: 4, boxSizing: "border-box", letterSpacing: "0.4px", color: "#D6791E", @@ -299,37 +297,37 @@ const MyView = (props) => { } const paperAppContainer = { - cursor: "pointer", display: "flex", flexWrap: 'wrap', alignContent: "space-between", } + + const paperAppStyle = { - minHeight: "148px", - width: "333px", - margin: "10px", + minHeight: 130, + width: "100%", color: "white", backgroundColor: surfaceColor, + padding: "12px 12px 0px 15px", borderRadius: 5, - padding: "10px", - cursor: "pointer", display: "flex", boxSizing: "border-box", + position: "relative", } const gridContainer = { - cursor: "pointer", height: "auto", color: "white", margin: "10px", backgroundColor: surfaceColor, } - const workFlowActionStyle = { + const workflowActionStyle = { flex: "1", display: "flex", - width: "150px", + width: 150, + height: 44, justifyContent: "space-between", overflow: "hidden" } @@ -535,23 +533,28 @@ const MyView = (props) => { }); } - const getWorkFlowMeta = (data) => { + const getWorkflowMeta = (data) => { + let triggers = 0 let schedules = 0 let webhooks = 0 - let webhookImg = "" - let scheduleImg = "" + let subflows = 0 if (data.triggers !== undefined && data.triggers !== null && data.triggers.length > 0) { + triggers = data.triggers.length for (let key in data.triggers) { + if (data.triggers[key].app_name === "Webhook") { webhooks += 1 - webhookImg = data.triggers[key].large_image + //webhookImg = data.triggers[key].large_image } else if (data.triggers[key].app_name === "Schedule") { schedules += 1 - scheduleImg = data.triggers[key].large_image + //scheduleImg = data.triggers[key].large_image + } else if (data.triggers[key].app_name === "Subflow") { + subflows += 1 } } } - return [schedules, webhooks, webhookImg, scheduleImg]; + + return [triggers, schedules, webhooks, subflows] } // dropdown with copy etc I guess @@ -565,9 +568,9 @@ const MyView = (props) => { boxWidth = "4px" } - var boxColor = "orange" + var boxColor = "#FECC00" if (data.is_valid) { - boxColor = "green" + boxColor = "#86c142" } const menuClick = (event) => { @@ -575,45 +578,57 @@ const MyView = (props) => { setAnchorEl(event.currentTarget); } - const actions = data.actions !== null ? data.actions.length : 0 - - const [schedules, webhooks] = getWorkFlowMeta(data); + var parsedName = data.name + console.log("LEN: ", parsedName.length) + if (parsedName !== undefined && parsedName !== null && parsedName.length > 25) { + parsedName = parsedName.slice(0,25)+".." + } + + + const actions = data.actions !== null ? data.actions.length : 0 + const [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data) - const imgSize = 25 return ( - { - }}> -
    { - if (selectedWorkflow.id !== data.id) { - setSelectedWorkflow(data) - //getWorkflowExecution(data.id) - } - }}/> - + + +
    - -
    { - if (selectedWorkflow.id !== data.id) { - setSelectedWorkflow(data) - //getWorkflowExecution(data.id) - } - }}> - - {data.name} - -
    + + + {parsedName} + - - - + + + + + + {actions} + + - - executeWorkflow(data.id)} /> + + + + + {triggers} + + + + + + + + + {subflows} + + + + {/* - {webhooks > 0 ? @@ -623,8 +638,9 @@ const MyView = (props) => { : null} + */} - + {data.tags !== undefined ? data.tags.map((tag, index) => { if (index >= 3) { @@ -634,7 +650,7 @@ const MyView = (props) => { return ( { : null} - {data.actions !== undefined && data.actions !== null ? - + - - - - { - setOpen(false) - setAnchorEl(null) - }} - > + + + + { + setOpen(false) + setAnchorEl(null) + }} + > { setModalOpen(true) setEditingWorkflow(data) @@ -685,26 +700,25 @@ const MyView = (props) => { exportWorkflow(data) setOpen(false) }} key={"export"}>{"Export"} - { + { setDeleteModalOpen(true) setSelectedWorkflowId(data.id) setOpen(false) }} key={"delete"}>{"Delete"} - - - - - - - - - + + + + + + + + + - : null} - + : null} + + ) } @@ -772,7 +786,6 @@ borderRadius: "4px", color: "black", height: "30px", "width": "30px", fontSize: marginTop: "5px", color: "white", backgroundColor: surfaceColor, - cursor: "pointer", display: "flex", } @@ -1294,13 +1307,12 @@ borderRadius: "4px", color: "black", height: "30px", "width": "30px", fontSize: let workflowData = ""; if (workflows.length > 0) { const columns = [ - { field: 'id', headerName: 'ID', width: 70, sortable: false, }, { field: 'title', headerName: 'Title', width: 330, }, { field: 'actions', headerName: 'Actions', width: 200, sortable: false, disableClickEventBubbling: true, renderCell: (params) => { const data = params.row.record; - let [schedules, webhooks] = getWorkFlowMeta(data); + let [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data); return @@ -1340,10 +1352,11 @@ borderRadius: "4px", color: "black", height: "30px", "width": "30px", fontSize: if (index >= 3) { return null } + return ( {view === "grid" && ( -
    + {workflows.map((data, index) => { return ( ) })} -
    +
    )} {view === "list" && ( diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index a955924a..9e477c29 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -447,12 +447,12 @@ const Workflows = (props) => { for (var branchkey in data.branches) { const branch = data.branches[branchkey] if (branch.source_id === data.actions[key].id) { - console.log("CHANGING SOURCE ID IN ACTION") + //console.log("CHANGING SOURCE ID IN ACTION") branch.source_id = newId } if (branch.destination_id === data.actions[key].id) { - console.log("CHANGING DESTINATION ID IN ACTION") + //console.log("CHANGING DESTINATION ID IN ACTION") branch.destination_id = newId } } @@ -505,10 +505,8 @@ const Workflows = (props) => { data = sanitizeWorkflow(data) alert.info("Sanitizing and publishing "+data.name) - const url = isCloud ? globalUrl : "https://shuffler.io" - // This ALWAYS talks to Shuffle cloud - fetch(url+"/api/v1/workflows/"+data.id+"/publish", { + fetch(globalUrl+"/api/v1/workflows/"+data.id+"/publish", { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -521,7 +519,11 @@ const Workflows = (props) => { if (response.status !== 200) { console.log("Status not 200 for workflow publish :O!") } else { - alert.success("Successfully published workflow") + if (isCloud) { + alert.success("Successfully published workflow") + } else { + alert.success("Successfully published workflow to https://shuffler.io") + } } return response.json() @@ -690,12 +692,10 @@ const Workflows = (props) => { setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags))) } }} key={"change"}>{"Change details"} - {isCloud ? - { - console.log("Should publish", data) - publishWorkflow(data) - }} key={"publish"}>{"Publish Workflow"} - : null} + { + console.log("Should publish", data) + publishWorkflow(data) + }} key={"publish"}>{"Publish Workflow"} { copyWorkflow(data) setOpen(false) diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 2ea4d555..48364d69 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-orborus -VERSION=0.8.64 +VERSION=0.8.70 echo "Running docker build with $NAME:$VERSION" #docker rmi frikky/shuffle:$NAME --force diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index ad508e8a..db337158 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -248,7 +248,7 @@ func initializeImages() { log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.8.64" + workerVersion = "0.8.70" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } diff --git a/functions/onprem/worker/Dockerfile b/functions/onprem/worker/Dockerfile index 62aa2d0c..826ef4d3 100644 --- a/functions/onprem/worker/Dockerfile +++ b/functions/onprem/worker/Dockerfile @@ -11,7 +11,8 @@ RUN go get github.com/docker/docker/api/types && \ go get github.com/docker/docker/client && \ go get github.com/gorilla/mux && \ go get github.com/patrickmn/go-cache && \ - go get github.com/frikky/shuffle-shared + go get github.com/frikky/shuffle-shared && \ + go get github.com/satori/go.uuid RUN go build RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker . @@ -21,7 +22,7 @@ FROM alpine:3.12 ENV SHUFFLE_BASE_IMAGE_REGISTRY=docker.io ENV SHUFFLE_BASE_IMAGE_NAME=frikky/shuffle -ENV SHUFFLE_BASE_IMAGE_TAG_SUFFIX=0.8.5 +ENV SHUFFLE_BASE_IMAGE_TAG_SUFFIX=0.8.70 RUN apk add --no-cache bash COPY --from=builder /app/ / diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 424b6207..ce343930 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,5 +1,5 @@ NAME=shuffle-worker -VERSION=0.8.64 +VERSION=0.8.70 echo "Running docker build with $NAME:$VERSION" #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index e4de1908..cc981636 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -24,6 +24,7 @@ import ( //"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/mount" dockerclient "github.com/docker/docker/client" + "github.com/satori/go.uuid" "github.com/gorilla/mux" "github.com/patrickmn/go-cache" @@ -278,7 +279,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] // Waiting to see if it exits.. Stupid, but stable(r) if workflowExecution.ExecutionSource != "default" { - log.Printf("Handling NON-default execution source %s - NOT waiting and validating!", workflowExecution.ExecutionSource) + log.Printf("[INFO] Handling NON-default execution source %s - NOT waiting and validating!", workflowExecution.ExecutionSource) } else if workflowExecution.ExecutionSource == "default" { time.Sleep(2 * time.Second) @@ -850,12 +851,13 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { appname = strings.Replace(appname, ".", "-", -1) appversion = strings.Replace(appversion, ".", "-", -1) - image := fmt.Sprintf("%s:%s_%s", baseimagename, action.AppName, action.AppVersion) + image := fmt.Sprintf("%s:%s_%s", baseimagename, strings.ToLower(action.AppName), action.AppVersion) if strings.Contains(image, " ") { image = strings.ReplaceAll(image, " ", "-") } - identifier := fmt.Sprintf("%s_%s_%s_%s", appname, appversion, action.ID, workflowExecution.ExecutionId) + // Added UUID to identifier just in case + identifier := fmt.Sprintf("%s_%s_%s_%s_%s", appname, appversion, action.ID, workflowExecution.ExecutionId, uuid.NewV4()) if strings.Contains(identifier, " ") { identifier = strings.ReplaceAll(identifier, " ", "-") } @@ -938,15 +940,15 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { } // Uses a few ways of getting / checking if an app is available - // 1. Try original - // 2. Go to lowercase + // 1. Try original with lowercase + // 2. Go to original // 3. Add remote repo location // 4. Actually download last repo images := []string{ - fmt.Sprintf("%s/%s:%s_%s", registryName, baseimagename, strings.ToLower(action.AppName), action.AppVersion), image, - fmt.Sprintf("%s:%s_%s", baseimagename, strings.ToLower(action.AppName), action.AppVersion), + fmt.Sprintf("%s:%s_%s", baseimagename, action.AppName, action.AppVersion), + fmt.Sprintf("%s/%s:%s_%s", registryName, baseimagename, strings.ToLower(action.AppName), action.AppVersion), } // If cleanup is set, it should run for efficiency @@ -959,6 +961,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { } log.Printf("[WARNING] Failed CLEANUP execution. Downloading image remotely.") + image = images[2] reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) if err != nil { log.Printf("[ERROR] Failed getting %s. The couldn't be find locally, AND is missing.", image) @@ -996,7 +999,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { } } else { - err = deployApp(dockercli, image, identifier, env, workflowExecution) + err = deployApp(dockercli, images[0], identifier, env, workflowExecution) if err != nil { if strings.Contains(err.Error(), "exited prematurely") { shutdown(workflowExecution, action.ID, err.Error(), true) @@ -1004,7 +1007,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { // Trying to replace with lowercase to deploy again. This seems to work with Dockerhub well. // FIXME: Should try to remotely download directly if this persists. - image = fmt.Sprintf("%s:%s_%s", baseimagename, strings.ToLower(action.AppName), action.AppVersion) + image = images[1] if strings.Contains(image, " ") { image = strings.ReplaceAll(image, " ", "-") } @@ -1015,7 +1018,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { shutdown(workflowExecution, action.ID, err.Error(), true) } - image = fmt.Sprintf("%s/%s:%s_%s", registryName, baseimagename, strings.ToLower(action.AppName), action.AppVersion) + image = images[2] if strings.Contains(image, " ") { image = strings.ReplaceAll(image, " ", "-") } @@ -1026,7 +1029,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { shutdown(workflowExecution, action.ID, err.Error(), true) } - log.Printf("[WARNING] Failed deploying image THRICE. Attempting to download the latter as last resort.") + log.Printf("[WARNING] Failed deploying image THREE TIMES. Attempting to download the latter as last resort.") reader, err := dockercli.ImagePull(context.Background(), image, pullOptions) if err != nil { log.Printf("[ERROR] Failed getting %s. The couldn't be find locally, AND is missing.", image) From 62d4c0f60276f7c0293caf13ec1f92fd6c2a79d8 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 30 Mar 2021 19:57:21 +0200 Subject: [PATCH 181/185] Downgraded baseimage to 0.8.64 --- backend/go-app/go.mod | 2 +- backend/go-app/go.sum | 6 + docker-compose.yml | 10 +- frontend/src/defaultCytoscapeStyle.js | 1 - frontend/src/views/AngularWorkflow.jsx | 392 ++++++++----- frontend/src/views/AppCreator.jsx | 4 +- frontend/src/views/MyView.jsx | 39 +- frontend/src/views/Workflows.jsx | 758 ++++++++++++++----------- 8 files changed, 698 insertions(+), 514 deletions(-) diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index fa5a2e35..4e64fc86 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -18,7 +18,7 @@ require ( github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect - github.com/frikky/shuffle-shared v0.0.20 + github.com/frikky/shuffle-shared v0.0.23 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index a20b5e4c..cc0824a0 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -90,6 +90,12 @@ github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww= github.com/frikky/shuffle-shared v0.0.15 h1:508ceeEHfPBMCC8/K4Zve3kwRQqiXNJSw6+BDoq9X4E= github.com/frikky/shuffle-shared v0.0.15/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww= +github.com/frikky/shuffle-shared v0.0.20 h1:y6JlPnQDq//elICWvVfUfJyU9gH3fSpQmPy+agqZ5sA= +github.com/frikky/shuffle-shared v0.0.20/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ= +github.com/frikky/shuffle-shared v0.0.21 h1:xj/XPsXTa2rx41mm4nUc7+2K9RGkq2/mpjSPtIfpjE4= +github.com/frikky/shuffle-shared v0.0.21/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ= +github.com/frikky/shuffle-shared v0.0.22 h1:TFMcJCNmOOSneMMWbg5dNzp2z6m0aZLROuL+bzVToRE= +github.com/frikky/shuffle-shared v0.0.22/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ= github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE= github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw= github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M= diff --git a/docker-compose.yml b/docker-compose.yml index 45656376..1e208eb2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: '3' services: frontend: - build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.70 + #build: ./frontend + image: ghcr.io/frikky/shuffle-frontend:0.8.64 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -16,8 +16,8 @@ services: depends_on: - backend backend: - build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.70 + #build: ./backend + image: ghcr.io/frikky/shuffle-backend:0.8.64 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -47,7 +47,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.70 + image: ghcr.io/frikky/shuffle-orborus:0.8.64 container_name: shuffle-orborus hostname: shuffle-orborus networks: diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index f2538a94..2732e381 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -55,7 +55,6 @@ const data = [{ css: { 'width': '30px', 'height': '30px', - 'font-size': '0px', }, }, { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 8082415d..dcc792fd 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -10,8 +10,7 @@ import { useBeforeunload } from 'react-beforeunload'; import NestedMenuItem from "material-ui-nested-menu-item"; import {TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade} from '@material-ui/core'; - -import {ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons'; +import {GetApp as GetAppIcon, Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons'; import * as cytoscape from 'cytoscape'; import * as edgehandles from 'cytoscape-edgehandles'; @@ -90,6 +89,8 @@ const AngularWorkflow = (props) => { const alert = useAlert() const borderRadius = 5 const theme = useTheme(); + const green = "#86c142" + const yellow = "#FECC00" const [bodyWidth, bodyHeight] = useWindowSize(); @@ -97,7 +98,6 @@ const AngularWorkflow = (props) => { const [cystyle, ] = useState(cytoscapestyle) const [cy, setCy] = React.useState() - const [appSearch, setAppSearch] = React.useState("") const [currentView, setCurrentView] = React.useState(0) const [triggerAuthentication, setTriggerAuthentication] = React.useState({}) const [triggerFolders, setTriggerFolders] = React.useState([]) @@ -284,14 +284,14 @@ const AngularWorkflow = (props) => { }) .then((response) => { if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") + console.log("Status not 200 for APIKEY gen :O!") } return response.json() }) .then((responseJson) => { setUserSettings(responseJson) - }) + }) .catch(error => { console.log(error) }); @@ -308,7 +308,7 @@ const AngularWorkflow = (props) => { }) .then((response) => { if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") + console.log("Status not 200 for get settings :O!") } return response.json() @@ -477,7 +477,7 @@ const AngularWorkflow = (props) => { }) .then((response) => { if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") + console.log("Status not 200 for ABORT EXECUTION :O!") } else { alert.success("Execution aborted") } @@ -798,6 +798,21 @@ const AngularWorkflow = (props) => { workflow.errors = responseJson.errors if (responseJson.errors.length === 0) { workflow.isValid = true + workflow.is_valid = true + + //console.log("ELEMENTS: ", cy.elements()) + //const setupGraph = () => { + const cyelements = cy.elements() + for (var i = 0; i < cyelements.length; i++) { + cyelements[i].removeStyle() + cyelements[i].data().is_valid = true + cyelements[i].data().errors = [] + } + + for (var key in workflow.actions) { + workflow.actions[key].is_valid = true + workflow.actions[key].errors = [] + } } for (var key in workflow.errors) { @@ -1049,7 +1064,7 @@ const AngularWorkflow = (props) => { setAuthLoaded(true) } else { setAuthLoaded(true) - alert.error("Failed getting authentications") + //alert.error("Failed getting authentications") } }) .catch(error => { @@ -1119,6 +1134,7 @@ const AngularWorkflow = (props) => { if (responseJson.isValid === undefined) { responseJson.isValid = true } + if (responseJson.errors === undefined) { responseJson.errors = [] } @@ -1219,10 +1235,21 @@ const AngularWorkflow = (props) => { } // Comparing locations between nodes and setting views - const onNodeDrag = (event, newAppAuth) => { + const onNodeDrag = (event) => { //console.log("DRAGGING: ", event.target) //console.log("LEN2: ", event.target.edges.length) + const nodedata = event.target.data() + if (nodedata.app_name == "Shuffle Tools" || nodedata.app_name == "Testing") { + //console.log("NODE: ", + //selector: `node[app_name="Shuffle Tools"]`, + console.log(event.target) + + // 1. Find location of node + // 2. Check if it's within view of another node (inside) + // 3. If it is, then hide text + } + /* event.target.animate({ @@ -1258,7 +1285,7 @@ const AngularWorkflow = (props) => { //const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id) console.log("NODE: ", data) - console.log("APPAUTH: ", newAppAuth) + //console.log("APPAUTH: ", newAppAuth) //console.log("BRANCHES: ", branch) if (data.type === "ACTION") { @@ -1682,15 +1709,21 @@ const AngularWorkflow = (props) => { return } + // App length necessary cus of cy initialization + //console.log("PRE ELEMENTS: !", workflow.actions, graphSetup, apps, authLoaded, cy) if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) { setGraphSetup(true) setupGraph() + + //console.log("IN ELEMENT CHECK!") } else if (!established && cy !== undefined && apps !== null && apps !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0 && authLoaded){ //This part has to load LAST, as it's kind of not async. //This means we need everything else to happen first. // //console.log("IN THIS PART AGAIN") + + //console.log("IN ESTABLISHED!") setEstablished(true) cy.edgehandles({ @@ -1719,7 +1752,7 @@ const AngularWorkflow = (props) => { cy.on('mouseout', 'node', (e) => onNodeHoverOut(e)) // Handles dragging - //cy.on('drag', 'node', (e) => onNodeDrag(e)) + cy.on('drag', 'node', (e) => onNodeDrag(e)) //cy.on('mouseover', 'node', () => $(targetElement).addClass('mouseover')); @@ -1801,6 +1834,7 @@ const AngularWorkflow = (props) => { node.data.type = "ACTION" node.isStartNode = action["id"] === workflow.start + var example = "" if (action.example !== undefined && action.example !== null && action.example.length > 0) { example = action.example @@ -1808,6 +1842,8 @@ const AngularWorkflow = (props) => { node.data.example = example + //node.data.is_valid = false + return node; }) @@ -2094,7 +2130,7 @@ const AngularWorkflow = (props) => {
    { }}> -
    +
    { setNewVariableName(variable.name) @@ -2161,11 +2197,12 @@ const AngularWorkflow = (props) => {
    { }}> -
    +
    { - setNewVariableName(variable.name) - setExecutionVariablesModalOpen(true)}}> + setNewVariableName(variable.name) + setExecutionVariablesModalOpen(true) + }}> Name: {variable.name}
    @@ -2228,7 +2265,7 @@ const AngularWorkflow = (props) => { const HandleLeftView = () => { // Defaults to apps. - var thisview = + var thisview = if (currentView === 1) { thisview = } else if (currentView === 2) { @@ -2379,7 +2416,7 @@ const AngularWorkflow = (props) => { : - const color = trigger.is_valid ? "green" : "orange" + const color = trigger.is_valid ? green : yellow return( { const appScrollStyle = { overflow: "scroll", - maxHeight: bodyHeight-appBarSize-55, - minHeight: bodyHeight-appBarSize-55, + maxHeight: bodyHeight-appBarSize-55-50, + minHeight: bodyHeight-appBarSize-55-50, + marginTop: 1, overflowY: "auto", overflowX: "hidden", } - const runAppSearch = (event) => { - setAppSearch(event.target.value) - setFilteredApps(apps.filter(app => app.name.toLowerCase().includes(event.target.value.trim().toLowerCase()))) - } + const AppView = (props) => { + const { allApps, prioritizedApps, filteredApps } = props; + const [visibleApps, setVisibleApps] = React.useState(prioritizedApps.concat(filteredApps.filter(innerapp => !internalIds.includes(innerapp.id)))) - const ParsedAppPaper = (props) => { - const app = props.app - const [hover, setHover] = React.useState(false) + const ParsedAppPaper = (props) => { + const app = props.app + const [hover, setHover] = React.useState(false) - // FIXME - add label to apps, as this might be slow with A LOT of apps - const maxlen = 24 - var newAppname = app.name.replace("_", " ", -1) - newAppname = newAppname.charAt(0).toUpperCase()+newAppname.substring(1) - if (newAppname.length > maxlen) { - newAppname = newAppname.slice(0, maxlen)+".." - } + const maxlen = 24 + var newAppname = app.name.replace("_", " ", -1) + newAppname = newAppname.charAt(0).toUpperCase()+newAppname.substring(1) + if (newAppname.length > maxlen) { + newAppname = newAppname.slice(0, maxlen)+".." + } - //const image = "url("+app.large_image+")" - const image = app.large_image - const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)) - const pixelSize = !hover ? "2px" : "4px" - newAppStyle.borderLeft = app.is_valid ? `${pixelSize} solid green` : `${pixelSize} solid orange` + //const image = "url("+app.large_image+")" + const image = app.large_image + const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)) + const pixelSize = !hover ? "2px" : "4px" + newAppStyle.borderLeft = app.is_valid ? `${pixelSize} solid ${green}` : `${pixelSize} solid ${yellow}` - return ( - {handleAppDrag(e, app)}} - onStop={(e) => {handleDragStop(e, app)}} - key={app.id} - dragging={false} - position={{ - x: 0, - y: 0, - }} - > - {setHover(true)}} onMouseOut={() => {setHover(false)}}> - - - {newAppname} - - - -

    {newAppname}

    + return ( + {handleAppDrag(e, app)}} + onStop={(e) => {handleDragStop(e, app)}} + key={app.id} + dragging={false} + position={{ + x: 0, + y: 0, + }} + > + {setHover(true)}} onMouseOut={() => {setHover(false)}}> + + + {newAppname} - - Version: {app.app_version} - - - {app.description} + + +

    {newAppname}

    +
    + + Version: {app.app_version} + + + {app.description} +
    -
    - ) - } + ) + } + + const runSearch = (event) => { + if (event.target.value.length > 0) { + setVisibleApps(allApps.filter(app => app.name.toLowerCase().includes(event.target.value.trim().toLowerCase()))) + } else { + setVisibleApps(prioritizedApps.concat(filteredApps.filter(innerapp => !internalIds.includes(innerapp.id)))) + } + } - const AppView = () => { return(
    + + + + + + ) + }} + fullWidth + color="primary" + placeholder={"Search Apps"} + id="appsearch" + onBlur={(event) => { + runSearch(event) + }} + />
    - {/* - { - runAppSearch(event) - }} - /> - */} - {prioritizedApps.map((app, index) => { - return( - - ) - })} - {filteredApps.filter(innerapp => !internalIds.includes(innerapp.id)).map((app, index) => { - if (app.invalid) { - return null - } + {visibleApps.map((app, index) => { + if (app.invalid) { + return null + } - return( - - ) - })} + return( + + ) + })}
    @@ -2979,10 +3022,10 @@ const AngularWorkflow = (props) => { const AppActionArguments = (props) => { const [selectedActionParameters, setSelectedActionParameters] = React.useState([]) const [selectedVariableParameter, setSelectedVariableParameter] = React.useState("") - const [showDropdown, setShowDropdown] = React.useState(false) - const [showDropdownNumber, setShowDropdownNumber] = React.useState(0) const [actionlist, setActionlist] = React.useState([]) const [jsonList, setJsonList] = React.useState([]) + const [showDropdown, setShowDropdown] = React.useState(false) + const [showDropdownNumber, setShowDropdownNumber] = React.useState(0) const [showAutocomplete, setShowAutocomplete] = React.useState(false) const [menuPosition, setMenuPosition] = useState(null) @@ -3865,6 +3908,7 @@ const AngularWorkflow = (props) => { } tmpitem = tmpitem.charAt(0).toUpperCase()+tmpitem.substring(1) + tmpitem = tmpitem.replaceAll("_", " ") const description = data.description === undefined ? "" : data.description return ( @@ -4121,7 +4165,7 @@ const AngularWorkflow = (props) => {
    -

    {selectedAction.app_name}

    +

    {selectedAction.app_name.replaceAll("_", " ")}

    { console.log("FIND EXAMPLE RESULTS FOR ", selectedAction) @@ -4969,7 +5013,7 @@ const AngularWorkflow = (props) => { return ( {}}> -
    +
    { setSourceValue(condition.source) @@ -5371,6 +5415,11 @@ const AngularWorkflow = (props) => { } const SubflowSidebar = () => { + const [menuPosition, setMenuPosition] = useState(null) + const [showDropdown, setShowDropdown] = React.useState(false) + const [showDropdownNumber, setShowDropdownNumber] = React.useState(0) + const [showAutocomplete, setShowAutocomplete] = React.useState(false) + if (Object.getOwnPropertyNames(selectedTrigger).length > 0) { if (workflow.triggers[selectedTriggerIndex] === undefined) { return null @@ -5485,48 +5534,50 @@ const AngularWorkflow = (props) => { Explore selected workflow } -
    -
    -
    - Select the Startnode -
    -
    {subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null : - { - setSubworkflowStartnode(e.target.value) - - try { - workflow.triggers[selectedTriggerIndex].parameters[3].value = e.target.value.id - } catch { - workflow.triggers[selectedTriggerIndex].parameters[3] = { - "name": "startnode", - "value": e.target.value.id, } - } + }} + fullWidth + onChange={(e) => { + setSubworkflowStartnode(e.target.value) - setWorkflow(workflow) - //setUpdate(Math.random()) - }} - style={{backgroundColor: inputColor, color: "white", height: "50px"}} - > - {subworkflow.actions.map((action, index) => { - //console.log(action) - return ( - parent.id === action.id)} key={index} style={{backgroundColor: inputColor, color: "white"}} value={action}> - {action.label} - - ) - })} - + try { + workflow.triggers[selectedTriggerIndex].parameters[3].value = e.target.value.id + } catch { + workflow.triggers[selectedTriggerIndex].parameters[3] = { + "name": "startnode", + "value": e.target.value.id, + } + } + + setWorkflow(workflow) + //setUpdate(Math.random()) + }} + style={{backgroundColor: inputColor, color: "white", height: "50px"}} + > + {subworkflow.actions.map((action, index) => { + //console.log(action) + return ( + parent.id === action.id)} key={index} style={{backgroundColor: inputColor, color: "white"}} value={action}> + {action.label} + + ) + })} + + }
    @@ -5543,6 +5594,21 @@ const AngularWorkflow = (props) => { maxWidth: "95%", fontSize: "1em", }, + endAdornment: ( + + + { + setMenuPosition({ + top: event.pageY+10, + left: event.pageX+10, + }) + //setShowDropdownNumber(count) + setShowDropdown(true) + setShowAutocomplete(true) + }}/> + + + ) }} rows="6" multiline @@ -6321,7 +6387,7 @@ const AngularWorkflow = (props) => { return null } - const cytoscapeViewWidths = 750 + const cytoscapeViewWidths = 800 const bottomBarStyle = { position: "fixed", right: 20, @@ -6551,7 +6617,7 @@ const AngularWorkflow = (props) => { @@ -6562,7 +6628,7 @@ const AngularWorkflow = (props) => { - + + {workflow.public ? + + + + + + : null} {/* */} {workflow.configuration !== null && workflow.configuration !== undefined && workflow.configuration.exit_on_error !== undefined ? : null}
    @@ -6830,7 +6918,7 @@ const AngularWorkflow = (props) => { {workflowExecutions.length > 0 ?
    {workflowExecutions.map((data, index) => { - const statusColor = data.status === "FINISHED" ? "green" : data.status === "ABORTED" || data.status === "FAILED" ? "red" : "orange" + const statusColor = data.status === "FINISHED" ? green : data.status === "ABORTED" || data.status === "FAILED" ? "red" : yellow const timeElapsed = data.completed_at-data.started_at const resultsLength = data.results !== undefined && data.results !== null ? data.results.length : 0 @@ -7006,7 +7094,7 @@ const AngularWorkflow = (props) => { const curapp = apps.find(a => a.name === data.action.app_name && a.app_version === data.action.app_version) const imgsize = 50 - const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? "green" : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : "orange" + const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? green : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : yellow var imgSrc = curapp === undefined ? "" : curapp.large_image if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) { @@ -7116,7 +7204,7 @@ const AngularWorkflow = (props) => { // This sucks :) const curapp = !codeModalOpen ? {} : selectedResult.action.app_name === "shuffle-subflow" ? triggers[1] : selectedResult.action.app_name === "User Input" ? triggers[2] : apps.find(a => a.name === selectedResult.action.app_name && a.app_version === selectedResult.action.app_version) const imgsize = 50 - const statusColor = !codeModalOpen ? "red" : selectedResult.status === "FINISHED" || selectedResult.status === "SUCCESS" ? "green" : selectedResult.status === "ABORTED" || selectedResult.status === "FAILURE" ? "red" : "orange" + const statusColor = !codeModalOpen ? "red" : selectedResult.status === "FINISHED" || selectedResult.status === "SUCCESS" ? green : selectedResult.status === "ABORTED" || selectedResult.status === "FAILURE" ? "red" : yellow const validate = !codeModalOpen ? "" : validateJson(selectedResult.result.trim()) if (validate.valid && typeof(validate.result) === "string") { validate.result = JSON.parse(validate.result) @@ -7287,7 +7375,7 @@ const AngularWorkflow = (props) => {
    - const newView = isLoggedIn ? + const newView = //isLoggedIn ?
    {leftView} @@ -7313,10 +7401,13 @@ const AngularWorkflow = (props) => {
    + + /* :
    TMP FOR NOT LOGGED IN
    + */ const executionVariableModal = executionVariablesModalOpen ? { : null - const loadedCheck = isLoaded && isLoggedIn && workflowDone ? + //const loadedCheck = isLoaded && isLoggedIn && workflowDone ? + const loadedCheck = isLoaded && workflowDone ?
    {newView} {variablesModal} diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 38f40744..bbf85301 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -1340,7 +1340,9 @@ const AppCreator = (props) => { placeholder={'Query name'} helperText={Click required switch} onBlur={(e) => { - urlPathQueries[index].name = e.target.value + console.log("IN BLUR: ", e.target.value) + urlPathQueries[index].name = e.target.value.replaceAll("=", "") + setUrlPathQueries(urlPathQueries) }} InputProps={{ diff --git a/frontend/src/views/MyView.jsx b/frontend/src/views/MyView.jsx index b456c6ed..70927e69 100644 --- a/frontend/src/views/MyView.jsx +++ b/frontend/src/views/MyView.jsx @@ -68,17 +68,21 @@ const flexContainerStyle = { } const flexBoxStyle = { - width: 333, height: 125, borderRadius: 4, boxSizing: "border-box", letterSpacing: "0.4px", color: "#D6791E", + margin: 10, + flex: 1, } -const activeWorkflowStyle = {backgroundColor: "#FFF5EE"} -const availableWorkflowStyle = {backgroundColor: "#F0F3FF"} -const notificationStyle = {backgroundColor: "#E5F9FF"} +//const activeWorkflowStyle = {backgroundColor: "#FFF5EE"} +//const notificationStyle = {backgroundColor: "#E5F9FF"} +//const activeWorkflowStyle = {backgroundColor: "#3d3f43"} +const availableWorkflowStyle = {backgroundColor: "#3d3f43"} +const notificationStyle = {backgroundColor: "#3d3f43"} +const activeWorkflowStyle = {backgroundColor: "#3d3f43"} const flexContentStyle = { display: "flex", @@ -302,8 +306,6 @@ const MyView = (props) => { alignContent: "space-between", } - - const paperAppStyle = { minHeight: 130, width: "100%", @@ -573,13 +575,16 @@ const MyView = (props) => { boxColor = "#86c142" } + if (!data.previously_saved) { + boxColor = "#f85a3e" + } + const menuClick = (event) => { setOpen(!open) setAnchorEl(event.currentTarget); } var parsedName = data.name - console.log("LEN: ", parsedName.length) if (parsedName !== undefined && parsedName !== null && parsedName.length > 25) { parsedName = parsedName.slice(0,25)+".." } @@ -589,7 +594,7 @@ const MyView = (props) => { const [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data) return ( - +
    @@ -608,7 +613,7 @@ const MyView = (props) => { - + {triggers} @@ -616,7 +621,7 @@ const MyView = (props) => { - + @@ -1420,10 +1425,6 @@ const MyView = (props) => {

    Workflows

    -

    THIS IS SOME WORKFLOW INFORMATION WHY ISN’T IT GROWING THE CORRECT WAY.

    -
    -
    - {workflowButtons}
    @@ -1457,6 +1458,15 @@ const MyView = (props) => {
    +
    +
    + This is your workflow view. Learn more about Workflows +
    +
    + {workflowButtons} +
    +
    +
    {view === "grid" && ( {workflows.map((data, index) => { @@ -1471,6 +1481,7 @@ const MyView = (props) => { )} +
    ) diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 9e477c29..0b30170e 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -1,8 +1,12 @@ import React, { useEffect} from 'react'; import { useInterval } from 'react-powerhooks'; +import { makeStyles } from '@material-ui/core/styles'; import {Grid, Paper, Tooltip, Divider, Button, TextField, FormControl, IconButton, Menu, MenuItem, FormControlLabel, Chip, Switch, Typography, Zoom, CircularProgress, Dialog, DialogTitle, DialogActions, DialogContent} from '@material-ui/core'; -import {Cached as CachedIcon, GetApp as GetAppIcon, Apps as AppsIcon, Edit as EditIcon, MoreVert as MoreVertIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Publish as PublishIcon, CloudDownload as CloudDownloadIcon} from '@material-ui/icons'; +import {FileCopy as FileCopyIcon, Delete as DeleteIcon, BubbleChart as BubbleChartIcon, Restore as RestoreIcon, Cached as CachedIcon, GetApp as GetAppIcon, Apps as AppsIcon, Edit as EditIcon, MoreVert as MoreVertIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Publish as PublishIcon, CloudUpload as CloudUploadIcon, CloudDownload as CloudDownloadIcon} from '@material-ui/icons'; +//import {Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons'; + +import {DataGrid, GridToolbarContainer, GridDensitySelector, GridToolbar} from '@material-ui/data-grid'; //import JSONPretty from 'react-json-pretty'; //import JSONPrettyMon from 'react-json-pretty/dist/monikai' @@ -13,10 +17,86 @@ import {Link} from 'react-router-dom'; import { useAlert } from "react-alert"; import ChipInput from 'material-ui-chip-input' import uuid from "uuid" +import CytoscapeWrapper from '../components/RenderCytoscape' + +//import mobileImage from '../assets/img/mobile.svg'; +//import bagImage from '../assets/img/bag.svg'; +//import bookImage from '../assets/img/book.svg'; const inputColor = "#383B40" const surfaceColor = "#27292D" +const flexContainerStyle = { + display: "flex", + flexDirection: "row", + justifyContent: "left", + alignContent: "space-between", +} + +const flexBoxStyle = { + height: 125, + borderRadius: 4, + boxSizing: "border-box", + letterSpacing: "0.4px", + color: "#D6791E", + margin: 10, + flex: 1, +} + +const useStyles = makeStyles((theme) => ({ + root: { + border: 0, + '& .MuiDataGrid-columnsContainer': { + backgroundColor: theme.palette.type === 'light' ? '#fafafa' : '#1d1d1d', + }, + '& .MuiDataGrid-iconSeparator': { + display: 'none', + }, + '& .MuiDataGrid-colCell, .MuiDataGrid-cell': { + borderRight: `1px solid ${ + theme.palette.type === 'light' ? 'white' : '#303030' + }`, + }, + '& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell': { + borderBottom: `1px solid ${ + theme.palette.type === 'light' ? '#f0f0f0' : '#303030' + }`, + }, + '& .MuiDataGrid-cell': { + color: + theme.palette.type === 'light' + ? 'white' + : 'rgba(255,255,255,0.65)', + }, + '& .MuiPaginationItem-root, .MuiTablePagination-actions, .MuiTablePagination-caption': { + borderRadius: 0, + color: "white", + }, + }, +})); + +//const activeWorkflowStyle = {backgroundColor: "#FFF5EE"} +//const notificationStyle = {backgroundColor: "#E5F9FF"} +//const activeWorkflowStyle = {backgroundColor: "#3d3f43"} +const availableWorkflowStyle = {backgroundColor: "#3d3f43"} +const notificationStyle = {backgroundColor: "#3d3f43"} +const activeWorkflowStyle = {backgroundColor: "#3d3f43"} + +const fontSize_16 = {fontSize: "16px",} +const counterStyle = {fontSize: "36px",fontWeight:"bold"} +const blockRightStyle = {textAlign: "right",padding: "20px 20px 0px 0px",width:"100%"} + +const flexContentStyle = { + display: "flex", + flexDirection: "row" +} + +const iconStyle = { + width: "75px", + height: "75px", + padding: "20px" +} + export const validateJson = (showResult) => { //showResult = showResult.split(" None").join(" \"None\"") showResult = showResult.split(" False").join(" false") @@ -54,6 +134,7 @@ const Workflows = (props) => { document.title = "Shuffle - Workflows" const alert = useAlert() + const classes = useStyles(); var upload = "" const [file, setFile] = React.useState(""); @@ -84,16 +165,9 @@ const Workflows = (props) => { const [executionLoading, setExecutionLoading] = React.useState(false) const [importLoading, setImportLoading] = React.useState(false) const [isDropzone, setIsDropzone] = React.useState(false); + const [view, setView] = React.useState("grid") const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" - const { start, stop } = useInterval({ - duration: 5000, - startImmediate: false, - callback: () => { - //getWorkflowExecution(selectedWorkflow.id) - } - }) - const deleteModal = deleteModalOpen ? { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for workflows :O!: ", response.status) + + if (isCloud) { + window.location.pathname = "/login" + } + alert.info("Failed getting workflows.") setWorkflowDone(true) @@ -244,7 +323,7 @@ const Workflows = (props) => { minWidth: 1024, maxWidth: 1024, margin: "auto", - maxHeight: "90vh", + /*maxHeight: "90vh",*/ } const emptyWorkflowStyle = { @@ -272,78 +351,38 @@ const Workflows = (props) => { overflowY: "auto", } + const paperAppContainer = { + display: "flex", + flexWrap: 'wrap', + alignContent: "space-between", + } + const paperAppStyle = { - minHeight: "100px", - minWidth: "100%", - maxWidth: "100%", - marginTop: "5px", + minHeight: 130, + width: "100%", color: "white", backgroundColor: surfaceColor, + padding: "12px 12px 0px 15px", borderRadius: 5, - padding: 10, - cursor: "pointer", display: "flex", + boxSizing: "border-box", + position: "relative", } - const getWorkflowExecution = (id) => { - setExecutionLoading(true) - fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - setExecutionLoading(false) - if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } - - return response.json() - }) - .then((responseJson) => { - if (responseJson.success === false) { - alert.error("Failed getting executions") - } else { - if (responseJson.length > 0) { - setSelectedExecution(responseJson[0]) - setWorkflowExecutions(responseJson) - } else { - //alert.info("Couldn't find executions for the workflow") - setSelectedExecution({}) - setWorkflowExecutions([]) - } - } - }) - .catch(error => { - setExecutionLoading(false) - alert.error(error.toString()) - }); + const gridContainer = { + height: "auto", + color: "white", + margin: "10px", + backgroundColor: surfaceColor, } - const abortExecution = (workflowid, executionid) => { - alert.success("Aborting execution") - fetch(globalUrl+"/api/v1/workflows/"+workflowid+"/executions/"+executionid+"/abort", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } - //getWorkflowExecution(workflowid) - - return response.json() - }) - .catch(error => { - alert.error(error.toString()) - }); + const workflowActionStyle = { + flex: "1", + display: "flex", + width: 150, + height: 44, + justifyContent: "space-between", + overflow: "hidden" } const executeWorkflow = (id) => { @@ -372,13 +411,6 @@ const Workflows = (props) => { .catch(error => { alert.error(error.toString()) }); - - if (id === selectedWorkflow.id) { - sleep(2000).then(() => { - stop() - start() - }) - } } function sleep (time) { @@ -393,7 +425,7 @@ const Workflows = (props) => { const sanitizeWorkflow = (data) => { data["owner"] = "" - console.log(data) + console.log("Sanitize start: ", data) if (data.triggers !== null && data.triggers !== undefined) { for (var key in data.triggers) { const trigger = data.triggers[key] @@ -457,7 +489,8 @@ const Workflows = (props) => { } } - data.actions[key].environment = isCloud ? "cloud" : "Shuffle" + //data.actions[key].environment = isCloud ? "cloud" : "Shuffle" + data.actions[key].environment = "" data.actions[key].id = newId } } @@ -482,16 +515,18 @@ const Workflows = (props) => { // These are backwards.. True = saved before. Very confuse. data["previously_saved"] = false data["first_save"] = false - console.log(data) + console.log("Sanitize end: ", data) return data } const exportWorkflow = (data) => { - console.log("export") let exportFileDefaultName = data.name+'.json'; data = sanitizeWorkflow(data) + //console.log("EXPORT: ", data) + //return + let dataStr = JSON.stringify(data) let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); let linkElement = document.createElement('a'); @@ -600,7 +635,6 @@ const Workflows = (props) => { }); } - // dropdown with copy etc I guess const WorkflowPaper = (props) => { const { data } = props; const [open, setOpen] = React.useState(false); @@ -611,12 +645,11 @@ const Workflows = (props) => { boxWidth = "4px" } - var boxColor = "#f85a3e" + var boxColor = "#FECC00" if (data.is_valid) { - boxColor = "green" + boxColor = "#86c142" } - //console.log(data) if (!data.previously_saved) { boxColor = "#f85a3e" } @@ -626,63 +659,110 @@ const Workflows = (props) => { setAnchorEl(event.currentTarget); } - const actions = data.actions !== null ? data.actions.length : 0 - var schedules = 0 - var webhooks = 0 - var webhookImg = "" - var scheduleImg = "" - if (data.triggers !== undefined && data.triggers !== null && data.triggers.length > 0) { - for (var key in data.triggers) { - if (data.triggers[key].app_name === "Webhook") { - webhooks += 1 - webhookImg = data.triggers[key].large_image - } else if (data.triggers[key].app_name === "Schedule") { - schedules += 1 - scheduleImg = data.triggers[key].large_image - } - } + var parsedName = data.name + if (parsedName !== undefined && parsedName !== null && parsedName.length > 25) { + parsedName = parsedName.slice(0,25)+".." } - const imgSize = 25 - //console.log("TOP INFO: ", data) - return ( - { - }}> -
    - - - -
    { - if (selectedWorkflow.id !== data.id) { - //setSelectedWorkflow(data) - //getWorkflowExecution(data.id) - } - }}> - - {data.name} - -
    -
    - - - - { - setOpen(false) - setAnchorEl(null) - }} - > + const actions = data.actions !== null ? data.actions.length : 0 + const [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data) + + return ( + + +
    + + + + {parsedName} + + + + + + + + {actions} + + + + + + + + {triggers} + + + + + + + + + + {subflows} + + + + {/* + + + + + + + : null} + {schedules > 0 ? + + + + : null} + */} + + + {data.tags !== undefined ? + data.tags.map((tag, index) => { + if (index >= 3) { + return null + } + + return ( + + ) + }) + : null} + + + {data.actions !== undefined && data.actions !== null ? + + + + + + { + setOpen(false) + setAnchorEl(null) + }} + > { setModalOpen(true) setEditingWorkflow(data) @@ -691,146 +771,57 @@ const Workflows = (props) => { if (data.tags !== undefined && data.tags !== null) { setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags))) } - }} key={"change"}>{"Change details"} + }} key={"change"}> + + {"Change details"} + { - console.log("Should publish", data) - publishWorkflow(data) - }} key={"publish"}>{"Publish Workflow"} + publishWorkflow(data) + }} key={"publish"}> + + {"Publish Workflow"} + { copyWorkflow(data) setOpen(false) - }} key={"duplicate"}>{"Duplicate Workflow"} + }} key={"duplicate"}> + + {"Duplicate Workflow"} + { exportWorkflow(data) setOpen(false) - }} key={"export"}>{"Export"} - { + }} key={"export"}> + + {"Export Workflow"} + + { setDeleteModalOpen(true) setSelectedWorkflowId(data.id) setOpen(false) - }} key={"delete"}>{"Delete"} + }} key={"delete"}> + + {"Delete Workflow"} + - -
    +
    -
    { - if (selectedWorkflow.id !== data.id) { - //setSelectedWorkflow(data) - //getWorkflowExecution(data.id) - } - }}> - - - - - - - - - - + + + + - {data.tags !== undefined ? - data.tags.map((tag, index) => { - if (index >= 3) { - return null - } - - return ( - - ) - }) - : null} - -
    - - - {data.actions !== undefined && data.actions !== null ? - - - - - - {webhooks > 0 ? - - {data.title} - - : null} - {schedules > 0 ? - - {data.title} - - : null} - - : null} - - ) - } - - const executionPaper = (data) => { - var boxWidth = "2px" - if (selectedExecution.execution_id === data.execution_id) { - boxWidth = "4px" - } - - var boxColor = "orange" - if (data.status === "ABORTED" || data.status === "UNFINISHED" || data.status === "FAILURE"){ - boxColor = "red" - } else if (data.status === "FINISHED") { - boxColor = "green" - } - - var t = new Date(data.started_at*1000) - if (data.workflow.actions === null || data.workflow.actions === undefined ) { - return null - } - - if (data.workflow.actions === null || data.workflow.actions === undefined) { - return null - } - - var actions = data.workflow.actions.length - if (data.results !== null) { - var results = data.results.length - } - - return ( - { - setSelectedExecution(data) - }}> -
    - - - -
    -

    Status: {data.status}

    - Actions: {results}/{actions} -
    -
    - -
    +
    -
    - - Started: {t.toISOString()} - -
    -
    - + : null} + + ) } + + const dividerColor = "rgb(225, 228, 232)" const resultPaperAppStyle = { @@ -1025,32 +1016,6 @@ const Workflows = (props) => { ) } - const ExecutionsView = () => { - if (workflowExecutions.length > 0) { - const sortedWorkflows = workflowExecutions.sort((a, b) => a.started_at - b.started_at).reverse() - - return ( -
    - {sortedWorkflows.map(data => { - return ( - executionPaper(data) - ) - })} -
    - ) - } - return ( - executionLoading ? -
    - -
    - : -

    - Executions have been moved to the Workflow itself.
    Click here to see them -

    - ) - } - // Can create and set workflows const setNewWorkflow = (name, description, tags, editingWorkflow, redirect) => { @@ -1173,6 +1138,111 @@ const Workflows = (props) => { setLoadWorkflowsModalOpen(false) } + const getWorkflowMeta = (data) => { + let triggers = 0 + let schedules = 0 + let webhooks = 0 + let subflows = 0 + if (data.triggers !== undefined && data.triggers !== null && data.triggers.length > 0) { + triggers = data.triggers.length + for (let key in data.triggers) { + + if (data.triggers[key].app_name === "Webhook") { + webhooks += 1 + //webhookImg = data.triggers[key].large_image + } else if (data.triggers[key].app_name === "Schedule") { + schedules += 1 + //scheduleImg = data.triggers[key].large_image + } else if (data.triggers[key].app_name === "Subflow") { + subflows += 1 + } + } + } + + return [triggers, schedules, webhooks, subflows] + } + + const WorkflowGridView = () => { + let workflowData = ""; + if (workflows.length > 0) { + const columns = [ + { field: 'title', headerName: 'Title', width: 330, }, + { field: 'actions', headerName: 'Actions', width: 200, sortable: false, + disableClickEventBubbling: true, + renderCell: (params) => { + const data = params.row.record; + let [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data); + + return + + + + + + + + executeWorkflow(data.id)} /> + + + + + {webhooks > 0 ? + + + + : null} + {schedules > 0 ? + + + + : null} + + } + }, + { field: 'tags', headerName: 'Tags', width: 390, sortable: false, + disableClickEventBubbling: true, + renderCell: (params) => { + const data = params.row.record; + return + {data.tags !== undefined ? + data.tags.map((tag, index) => { + if (index >= 3) { + return null + } + + return ( + + ) + }) + : null} + + } + }, + ]; + let rows = []; + rows = workflows.map((data, index) => { + let obj = {"id":index+1, "title":data.name, "record":data,}; + return obj; + }); + workflowData = + } + return ( +
    + {workflowData} +
    + ); + } + const modalView = modalOpen ? {
    -
    -

    Workflows ({workflows.length})

    +
    +

    Workflows

    -
    +
    + + {/* +
    +
    +
    +
    +
    +
    {workflows.length}
    +
    ACTIVE WORKFLOWS
    +
    +
    +
    +
    +
    +
    +
    +
    {workflows.length}
    +
    AVAILABE WORKFLOWS
    +
    +
    +
    +
    +
    +
    +
    +
    {workflows.length}
    +
    NOTIFICATIONS
    +
    +
    +
    +
    + */} + +
    +
    + This is your workflow view. Learn more about Workflows +
    +
    {workflowButtons}
    - - -
    - {workflows.map((data, index) => { - return ( +
    + {view === "grid" && ( + + {workflows.map((data, index) => { + return ( - ) - })} -
    + ) + })} + + )} + + {view === "list" && ( + + )} + +
    -
    -
    -
    -

    Executions: {selectedWorkflow.name}

    -
    - {/* -
    - -
    - */} -
    - -
    - -
    -
    - {/* -
    -
    -
    -

    Execution Timeline

    -
    -
    - Collapse results
    } - control={ {setCollapseJson(!collapseJson)}} />} - /> -
    -
    - -
    - -
    -
    - */}
    ) } From 52a45268b60463710c3dcb9af383a3185639d05c Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 30 Mar 2021 19:57:43 +0200 Subject: [PATCH 182/185] Added cytoscape renderer --- frontend/src/components/RenderCytoscape.js | 146 +++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 frontend/src/components/RenderCytoscape.js diff --git a/frontend/src/components/RenderCytoscape.js b/frontend/src/components/RenderCytoscape.js new file mode 100644 index 00000000..3a88dcf8 --- /dev/null +++ b/frontend/src/components/RenderCytoscape.js @@ -0,0 +1,146 @@ +import React, {useState, useEffect, useLayoutEffect} from 'react'; +import * as cytoscape from 'cytoscape'; +import CytoscapeComponent from 'react-cytoscapejs'; +import cystyle from '../defaultCytoscapeStyle'; + +const surfaceColor = "#27292D" +const CytoscapeWrapper = (props) => { + const { globalUrl, inworkflow } = props; + + const [elements, setElements] = useState([]) + const [workflow, setWorkflow] = useState(inworkflow) + const [cy, setCy] = React.useState() + const bodyWidth = 200 + const bodyHeight = 150 + + const setupGraph = () => { + const actions = workflow.actions.map(action => { + const node = {} + node.position = action.position + node.data = action + + node.data._id = action["id"] + node.data.type = "ACTION" + node.isStartNode = action["id"] === workflow.start + + + var example = "" + if (action.example !== undefined && action.example !== null && action.example.length > 0) { + example = action.example + } + + node.data.example = example + return node; + }) + + const triggers = workflow.triggers.map(trigger => { + const node = {} + node.position = trigger.position + node.data = trigger + + node.data._id = trigger["id"] + node.data.type = "TRIGGER" + + return node; + }) + + // FIXME - tmp branch update + var insertedNodes = [].concat(actions, triggers) + const edges = workflow.branches.map((branch, index) => { + //workflow.branches[index].conditions = [{ + + const edge = { }; + var conditions = workflow.branches[index].conditions + if (conditions === undefined || conditions === null) { + conditions = [] + } + + var label = "" + if (conditions.length === 1) { + label = conditions.length+" condition" + } else if (conditions.length > 1) { + label = conditions.length+" conditions" + } + + edge.data = { + id: branch.id, + _id: branch.id, + source: branch.source_id, + target: branch.destination_id, + label: label, + conditions: conditions, + hasErrors: branch.has_errors + }; + + // This is an attempt at prettier edges. The numbers are weird to work with. + /* + //http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html + const sourcenode = actions.find(node => node.data._id === branch.source_id) + const destinationnode = actions.find(node => node.data._id === branch.destination_id) + if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) { + //node.data._id = action["id"] + console.log("SOURCE: ", sourcenode.position) + console.log("DESTINATIONNODE: ", destinationnode.position) + + var opposite = true + if (sourcenode.position.x > destinationnode.position.x) { + opposite = false + } else { + opposite = true + } + + edge.style = { + 'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"], + 'control-point-weight': ['0.3', '0.7'], + } + } + */ + + return edge; + }) + + setWorkflow(workflow) + + // Verifies if a branch is valid and skips others + var newedges = [] + for (var key in edges) { + var item = edges[key] + + const sourcecheck = insertedNodes.find(data => data.data.id === item.data.source) + const destcheck = insertedNodes.find(data => data.data.id === item.data.target) + if (sourcecheck === undefined || destcheck === undefined) { + continue + } + + newedges.push(item) + } + + insertedNodes = insertedNodes.concat(newedges) + setElements(insertedNodes) + } + + if (elements.length === 0) { + setupGraph() + } + + return ( + { + // FIXME: There's something specific loading when + // you do the first hover of a node. Why is this different? + //console.log("CY: ", incy) + setCy(incy) + }} + /> + ) +} + +export default CytoscapeWrapper From 92753f0be791f64624c3e459c96347340cd1b70f Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 31 Mar 2021 09:24:19 +0200 Subject: [PATCH 183/185] Added filtering to workflow view --- frontend/src/views/Workflows.jsx | 123 ++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 9 deletions(-) diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 0b30170e..a2655857 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -140,6 +140,7 @@ const Workflows = (props) => { const [file, setFile] = React.useState(""); const [workflows, setWorkflows] = React.useState([]); + const [filteredWorkflows, setFilteredWorkflows] = React.useState([]); const [selectedWorkflow, setSelectedWorkflow] = React.useState({}); const [selectedExecution, setSelectedExecution] = React.useState({}); const [workflowExecutions, setWorkflowExecutions] = React.useState([]); @@ -166,8 +167,81 @@ const Workflows = (props) => { const [importLoading, setImportLoading] = React.useState(false) const [isDropzone, setIsDropzone] = React.useState(false); const [view, setView] = React.useState("grid") + const [filters, setFilters] = React.useState([]) const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" + const findWorkflow = (filters) => { + if (filters.length === 0) { + setFilteredWorkflows(workflows) + return + } + + var newWorkflows = [] + for (var workflowKey in workflows) { + const curWorkflow = workflows[workflowKey] + + var found = [false] + if (curWorkflow.tags === undefined || curWorkflow.tags === null) { + found = filters.map(filter => curWorkflow.name.toLowerCase().includes(filter)) + } else { + found = filters.map(filter => curWorkflow.name.toLowerCase().includes(filter.toLowerCase()) || curWorkflow.tags.includes(filter)) + } + //console.log("FOUND: ", found) + //if (found) { + if (found.every(v => v === true)) { + newWorkflows.push(curWorkflow) + continue + } + } + + if (newWorkflows.length !== workflows.length) { + setFilteredWorkflows(newWorkflows) + } + } + + const addFilter = (data) => { + if (data === null || data === undefined) { + return + } + + if (data.includes("<") && data.includes(">")) { + return + } + + if (filters.includes(data)) { + return + } + + filters.push(data.toLowerCase()) + setFilters(filters) + + findWorkflow(filters) + } + + const removeFilter = (index) => { + var newfilters = filters + + if (index < 0) { + console.log("Can't handle index: ", index) + return + } + + + //console.log("Removing filter index", index) + newfilters.splice(index, 1) + //console.log("FILTER LENGTH: ", filters.length) + + if (newfilters.length === 0) { + newfilters = [] + setFilters(newfilters) + } else { + setFilters(newfilters) + } + //console.log("FILTERS: ", newfilters) + + findWorkflow(newfilters) + } + const deleteModal = deleteModalOpen ? { if (responseJson !== undefined) { setWorkflows(responseJson) + setFilteredWorkflows(responseJson) setWorkflowDone(true) } else { if (isLoggedIn) { @@ -635,6 +710,11 @@ const Workflows = (props) => { }); } + + const handleChipClick = (e) => { + addFilter(e.target.innerHTML) + } + const WorkflowPaper = (props) => { const { data } = props; const [open, setOpen] = React.useState(false); @@ -674,8 +754,10 @@ const Workflows = (props) => {
    - - {parsedName} + + + {parsedName} + @@ -732,6 +814,7 @@ const Workflows = (props) => { key={index} style={{backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",}} label={tag} + onClick={handleChipClick} variant="outlined" color="primary" /> @@ -809,7 +892,7 @@ const Workflows = (props) => { - + @@ -1180,7 +1263,7 @@ const Workflows = (props) => { borderRadius: "4px", color: "black", height: "20px", "width": "20px", fontSize: "small"}} /> - + executeWorkflow(data.id)} /> @@ -1296,10 +1379,11 @@ const Workflows = (props) => { fullWidth /> {
    -
    +

    Workflows

    +
    + { + addFilter(chip) + }} + onDelete={(chip, index) => { + removeFilter(index) + //setUpdate("delete "+chip) + }} + /> +
    {/* @@ -1475,7 +1580,7 @@ const Workflows = (props) => {
    - This is your workflow view. Learn more about Workflows + Your workflow view. Learn more about Workflows
    {workflowButtons} @@ -1484,7 +1589,7 @@ const Workflows = (props) => {
    {view === "grid" && ( - {workflows.map((data, index) => { + {filteredWorkflows.map((data, index) => { return ( ) @@ -1600,7 +1705,7 @@ const Workflows = (props) => { onChange={e => setDownloadUrl(e.target.value)} placeholder="https://github.com/frikky/shuffle-apps" fullWidth - /> + /> Branch (default value is "master"):
    From ab5c0d82b984a90ce17cc467f9d6cacecfffc86a Mon Sep 17 00:00:00 2001 From: frikky Date: Wed, 31 Mar 2021 20:21:45 +0200 Subject: [PATCH 184/185] Added new colors and workflow view --- backend/app_sdk/app_base.py | 6 +- backend/go-app/go.sum | 2 + docker-compose.yml | 8 +- frontend/src/App.jsx | 27 +--- frontend/src/defaultCytoscapeStyle.js | 3 +- frontend/src/views/AngularWorkflow.jsx | 201 ++++++++++++++++--------- frontend/src/views/Apps.jsx | 90 ++++++----- frontend/src/views/Workflows.jsx | 131 ++++++++++------ functions/onprem/orborus/build.sh | 2 +- functions/onprem/orborus/go.mod | 2 +- functions/onprem/orborus/orborus.go | 4 +- functions/onprem/worker/build.sh | 2 +- functions/onprem/worker/go.mod | 18 +++ 13 files changed, 301 insertions(+), 195 deletions(-) create mode 100644 functions/onprem/worker/go.mod diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 5e2cdd3e..c3bc15c4 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1543,7 +1543,7 @@ class AppBase: sourcevalue = condition["source"]["value"] check, sourcevalue, is_loop = parse_params(action, fullexecution, condition["source"]) if check: - return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check) + return False, {"success": False, "reason": "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)} #sourcevalue = sourcevalue.encode("utf-8") @@ -1552,7 +1552,7 @@ class AppBase: check, destinationvalue, is_loop = parse_params(action, fullexecution, condition["destination"]) if check: - return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check) + return False, {"success": False, "reason": "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)} #destinationvalue = destinationvalue.encode("utf-8") destinationvalue = parse_wrapper_start(destinationvalue) @@ -1593,7 +1593,7 @@ class AppBase: if not validation: self.logger.info("Failed condition check for %s %s %s." % (sourcevalue, condition["condition"]["value"], destinationvalue)) - return False, "Failed condition: %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue) + return False, {"success": False, "reason": "Failed condition: %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue)} # Make a general parser here, at least to get param["name"] = param["value"] in maparameter[string]string diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index cc0824a0..21fd33ff 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -96,6 +96,8 @@ github.com/frikky/shuffle-shared v0.0.21 h1:xj/XPsXTa2rx41mm4nUc7+2K9RGkq2/mpjSP github.com/frikky/shuffle-shared v0.0.21/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ= github.com/frikky/shuffle-shared v0.0.22 h1:TFMcJCNmOOSneMMWbg5dNzp2z6m0aZLROuL+bzVToRE= github.com/frikky/shuffle-shared v0.0.22/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ= +github.com/frikky/shuffle-shared v0.0.23 h1:Pnlc2M6fHnFRLFd5K1iLTVv4/t4P04Ri1GJ5CMxzq0U= +github.com/frikky/shuffle-shared v0.0.23/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ= github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE= github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw= github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M= diff --git a/docker-compose.yml b/docker-compose.yml index 1e208eb2..56fd9a3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:0.8.64 + image: ghcr.io/frikky/shuffle-frontend:0.8.71 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:0.8.64 + image: ghcr.io/frikky/shuffle-backend:0.8.71 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -47,7 +47,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: ghcr.io/frikky/shuffle-orborus:0.8.64 + image: ghcr.io/frikky/shuffle-orborus:0.8.71 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -56,7 +56,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - SHUFFLE_APP_SDK_VERSION=0.8.60 - - SHUFFLE_WORKER_VERSION=0.8.70 + - SHUFFLE_WORKER_VERSION=0.8.71 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 011a4c6e..99465936 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,7 @@ import EditWebhook from "./views/EditWebhook"; import AngularWorkflow from "./views/AngularWorkflow"; import Header from './components/Header'; +import theme from './theme' import Apps from './views/Apps'; import AppCreator from './views/AppCreator'; @@ -44,34 +45,8 @@ if (window.location.protocol == "http:" && window.location.port === "3000") { //globalUrl = "http://localhost:5002" } -const theme = createMuiTheme({ - palette: { - primary: { - main: "#f85a3e" - }, - secondary: { - main: '#e8eaf6', - }, - surfaceColor: "#27292d", - inputColor: "#383B40" - }, - typography: { - useNextVariants: true - }, - overrides: { - MuiMenu: { - list: { - backgroundColor: "#383B40", - }, - }, - }, -}); - - -// FIXME - set client side cookies const App = (message, props) => { const [userdata, setUserData] = useState({}); - //const [homePage, ] = useState(true); const [cookies, setCookie, removeCookie] = useCookies([]); const [isLoggedIn, setIsLoggedIn] = useState(false); const [dataset, setDataset] = useState(false); diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index 2732e381..8fd5d326 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -47,7 +47,6 @@ const data = [{ css: { 'width': '30px', 'height': '30px', - 'font-size': '0px', }, }, { @@ -118,6 +117,8 @@ const data = [{ css: { 'shape': 'ellipse', 'border-color': '#80deea', + 'width': '80px', + 'height': '80px', }, }, { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index dcc792fd..bb6e1d40 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2777,11 +2777,12 @@ const AngularWorkflow = (props) => { const [hover, setHover] = React.useState(false) const maxlen = 24 - var newAppname = app.name.replace("_", " ", -1) + var newAppname = app.name newAppname = newAppname.charAt(0).toUpperCase()+newAppname.substring(1) if (newAppname.length > maxlen) { newAppname = newAppname.slice(0, maxlen)+".." } + app.name.replaceAll("_", " ", -1) //const image = "url("+app.large_image+")" const image = app.large_image @@ -2807,13 +2808,17 @@ const AngularWorkflow = (props) => { -

    {newAppname}

    + {newAppname}
    - Version: {app.app_version} + + Version: {app.app_version} + - {app.description} + + {app.description} +
    @@ -2833,43 +2838,52 @@ const AngularWorkflow = (props) => { return(
    - - - - - - ) - }} - fullWidth - color="primary" - placeholder={"Search Apps"} - id="appsearch" - onBlur={(event) => { - runSearch(event) - }} - /> -
    - {visibleApps.map((app, index) => { - if (app.invalid) { - return null - } - - return( - + + + + + ) - })} -
    + }} + fullWidth + color="primary" + placeholder={"Search Active Apps"} + id="appsearch" + onBlur={(event) => { + runSearch(event) + }} + /> + {visibleApps.length > 0 ? +
    + {visibleApps.map((app, index) => { + if (app.invalid) { + return null + } + + return( + + ) + })} +
    + : +
    + + + Loading apps + +
    + }
    ) @@ -2925,13 +2939,13 @@ const AngularWorkflow = (props) => { // const selectedNameChange = (event) => { //console.log("OLDNAME: ", selectedAction.name) - event.target.value = event.target.value.replace("(", "") - event.target.value = event.target.value.replace(")", "") - event.target.value = event.target.value.replace("$", "") - event.target.value = event.target.value.replace("#", "") - event.target.value = event.target.value.replace(".", "") - event.target.value = event.target.value.replace(",", "") - event.target.value = event.target.value.replace(" ", "_") + event.target.value = event.target.value.replaceAll("(", "") + event.target.value = event.target.value.replaceAll(")", "") + event.target.value = event.target.value.replaceAll("$", "") + event.target.value = event.target.value.replaceAll("#", "") + event.target.value = event.target.value.replaceAll(".", "") + event.target.value = event.target.value.replaceAll(",", "") + event.target.value = event.target.value.replaceAll(" ", "_") selectedAction.label = event.target.value setSelectedAction(selectedAction) @@ -4384,10 +4398,10 @@ const AngularWorkflow = (props) => { newActionname = data.label } // ROFL FIXME - loop - newActionname = newActionname.replace("_", " ") - newActionname = newActionname.replace("_", " ") - newActionname = newActionname.replace("_", " ") - newActionname = newActionname.replace("_", " ") + newActionname = newActionname.replaceAll("_", " ") + newActionname = newActionname.replaceAll("_", " ") + newActionname = newActionname.replaceAll("_", " ") + newActionname = newActionname.replaceAll("_", " ") newActionname = newActionname.charAt(0).toUpperCase()+newActionname.substring(1) return ( @@ -6877,7 +6891,7 @@ const AngularWorkflow = (props) => { // console.log("HANDLE INPUT FIELD FOR COPY!") //} - to_be_copied.replace(" ", "_") + to_be_copied.replaceAll(" ", "_") const elementName = "copy_element_shuffle" var copyText = document.getElementById(elementName); if (copyText !== null && copyText !== undefined) { @@ -7023,37 +7037,59 @@ const AngularWorkflow = (props) => {
    {executionData.status !== undefined && executionData.status.length > 0 ? -
    - Status:   {executionData.status} +
    + + Status   + + + {executionData.status} +
    : null } {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ? -
    - Source:   {executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ? - executionData.execution_source === props.match.params.key ? - { - getWorkflowExecution(props.match.params.key, executionData.execution_parent) - }}> - Parent Execution - +
    + + Source    + + + {executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ? + executionData.execution_source === props.match.params.key ? + + { + getWorkflowExecution(props.match.params.key, executionData.execution_parent) + }}> + Parent Execution + + + : + Parent Workflow : - Parent Workflow - : - executionData.execution_source - } + executionData.execution_source + } +
    : null } {executionData.started_at !== undefined ? -
    - Started:  {new Date(executionData.started_at*1000).toISOString()} +
    + + Started    + + + {new Date(executionData.started_at*1000).toISOString()} +
    : null } {executionData.completed_at !== undefined && executionData.completed_at !== null && executionData.completed_at > 0 ? -
    - Finished: {new Date(executionData.completed_at*1000).toISOString()} +
    + + Finished   + + + {new Date(executionData.completed_at*1000).toISOString()} +
    : null } @@ -7153,10 +7189,21 @@ const AngularWorkflow = (props) => { {actionimg}
    {data.action.label}
    -
    {data.action.name}
    +
    + + {data.action.name} + +
    -
    Status {data.status}
    +
    + + Status   + + + {data.status} + +
    {validate.valid ? { } : -
    - Result  - {data.result} +
    + + Result  + + + {data.result} +
    }
    diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 642989d5..40d2cb4e 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -16,6 +16,10 @@ import Dropzone from '../components/Dropzone'; const surfaceColor = "#27292D" const inputColor = "#383B40" +const chipStyle = { + backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", +} + // Parses JSON data into keys that can be used everywhere :) export const GetParsedPaths = (inputdata, basekey) => { const splitkey = " > " @@ -173,7 +177,7 @@ const Apps = (props) => { minHeight: 130, maxHeight: 130, minWidth: "100%", - maxWidth: "100%", + maxWidth: 612.5, marginBottom: 5, borderRadius: 5, color: "white", @@ -341,11 +345,9 @@ const Apps = (props) => { //console.log("IMG LOADED!: ", event.target) }} /> - // FIXME - add label to apps, as this might be slow with A LOT of apps var newAppname = data.name - newAppname = newAppname.replace("_", " ") newAppname = newAppname.charAt(0).toUpperCase()+newAppname.substring(1) - + newAppname = newAppname.replaceAll("_", " ") var sharing = "public" if (!data.sharing) { sharing = "private" @@ -361,7 +363,7 @@ const Apps = (props) => { } var description = data.description - const maxDescLen = 51 + const maxDescLen = 60 if (description.length > maxDescLen) { description = data.description.slice(0, maxDescLen)+"..." } @@ -370,6 +372,7 @@ const Apps = (props) => { return ( { if (selectedApp.id !== data.id) { + data.name = newAppname setSelectedApp(data) console.log(data) @@ -389,17 +392,21 @@ const Apps = (props) => { {imageline}
    - - + + -

    {newAppname}

    + + {newAppname} +
    -
    - - {description} +
    + + + {description} +
    - + {data.tags === null || data.tags === undefined ? null : data.tags.map((tag, index) => { if (index >= 3) { return null @@ -408,7 +415,7 @@ const Apps = (props) => { return ( { // FIXME - add label to apps, as this might be slow with A LOT of apps var newAppname = selectedApp.name if (newAppname !== undefined && newAppname.length > 0) { - newAppname = newAppname.replace("_", " ") + newAppname = newAppname.replaceAll("_", " ") newAppname = newAppname.charAt(0).toUpperCase()+newAppname.substring(1) } else { newAppname = "" @@ -613,9 +620,15 @@ const Apps = (props) => { {imageline}
    -

    {newAppname}

    -

    Version {selectedApp.app_version}

    -

    {description}

    + + {newAppname} + + + Version {selectedApp.app_version} + + + {description} +
    {isCloud ? @@ -644,7 +657,7 @@ const Apps = (props) => { return ( { {props.userdata !== undefined && props.userdata.id === selectedApp.owner ?
    {/*

    ID: {selectedApp.id}

    */} - Sharing: + Sharing