Merge pull request #72 from frikky/1.0.0
Execution model overhaul. Executions can now start ~anywhere.
This commit is contained in:
@@ -18,3 +18,4 @@ functions/generated_apps
|
||||
|
||||
backend/onprem/app_sdk/apps
|
||||
*test.py
|
||||
shuffle-apps/*
|
||||
|
||||
+288
-22
@@ -5,6 +5,7 @@ import time
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import urllib.parse
|
||||
|
||||
class AppBase:
|
||||
""" The base class for Python-based apps in Shuffle, handles logging and callbacks configurations"""
|
||||
@@ -102,6 +103,208 @@ class AppBase:
|
||||
|
||||
self.logger.info("AFTER FULLEXEC stream result")
|
||||
|
||||
# Gets the value at the paranthesis level you want
|
||||
def parse_nested_param(string, level):
|
||||
"""
|
||||
Generate strings contained in nested (), indexing i = level
|
||||
"""
|
||||
if len(re.findall("\(", string)) == len(re.findall("\)", string)):
|
||||
LeftRightIndex = [x for x in zip(
|
||||
[Left.start()+1 for Left in re.finditer('\(', string)],
|
||||
reversed([Right.start() for Right in re.finditer('\)', string)]))]
|
||||
|
||||
elif len(re.findall("\(", string)) > len(re.findall("\)", string)):
|
||||
return parse_nested_param(string + ')', level)
|
||||
elif len(re.findall("\(", string)) < len(re.findall("\)", string)):
|
||||
return parse_nested_param('(' + string, level)
|
||||
|
||||
else:
|
||||
return 'Failed to parse params'
|
||||
|
||||
try:
|
||||
return [string[LeftRightIndex[level][0]:LeftRightIndex[level][1]]]
|
||||
except IndexError:
|
||||
return [string[LeftRightIndex[level+1][0]:LeftRightIndex[level+1][1]]]
|
||||
|
||||
# Finds the deepest level paranthesis in a string
|
||||
def maxDepth(S):
|
||||
current_max = 0
|
||||
max = 0
|
||||
n = len(S)
|
||||
|
||||
# Traverse the input string
|
||||
for i in range(n):
|
||||
if S[i] == '(':
|
||||
current_max += 1
|
||||
|
||||
if current_max > max:
|
||||
max = current_max
|
||||
elif S[i] == ')':
|
||||
if current_max > 0:
|
||||
current_max -= 1
|
||||
else:
|
||||
return -1
|
||||
|
||||
# finally check for unbalanced string
|
||||
if current_max != 0:
|
||||
return -1
|
||||
|
||||
return max-1
|
||||
|
||||
# Specific type parsing
|
||||
def parse_type(data, thistype):
|
||||
if data == None:
|
||||
return "Empty"
|
||||
|
||||
if "int" in thistype or "number" in thistype:
|
||||
try:
|
||||
return int(data)
|
||||
except ValueError:
|
||||
print("ValueError while casting %s" % data)
|
||||
return data
|
||||
if "lower" in thistype:
|
||||
return data.lower()
|
||||
if "upper" in thistype:
|
||||
return data.upper()
|
||||
if "trim" in thistype:
|
||||
return data.strip()
|
||||
if "strip" in thistype:
|
||||
return data.strip()
|
||||
if "split" in thistype:
|
||||
return data.split()
|
||||
if "len" in thistype or "length" in thistype:
|
||||
return len(data)
|
||||
if "parse" in thistype:
|
||||
splitvalues = []
|
||||
default_error = """Error. Expected syntax: parse(["hello","test1"],0:1)"""
|
||||
if "," in data:
|
||||
splitvalues = data.split(",")
|
||||
|
||||
for item in range(len(splitvalues)):
|
||||
splitvalues[item] = splitvalues[item].strip()
|
||||
else:
|
||||
return default_error
|
||||
|
||||
lastsplit = []
|
||||
if ":" in splitvalues[-1]:
|
||||
lastsplit = splitvalues[-1].split(":")
|
||||
else:
|
||||
try:
|
||||
lastsplit = [int(splitvalues[-1])]
|
||||
except ValueError:
|
||||
return default_error
|
||||
|
||||
try:
|
||||
parsedlist = ",".join(splitvalues[0:-1])
|
||||
if len(lastsplit) > 1:
|
||||
tmp = json.loads(parsedlist)[int(lastsplit[0]):int(lastsplit[1])]
|
||||
else:
|
||||
tmp = json.loads(parsedlist)[lastsplit[0]]
|
||||
|
||||
print(tmp)
|
||||
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
|
||||
except TypeError:
|
||||
return data
|
||||
|
||||
print("Running %s" % data)
|
||||
|
||||
# Look for the INNER wrapper first, then move out
|
||||
wrappers = ["int", "number", "lower", "upper", "trim", "strip", "split", "parse", "len", "length"]
|
||||
found = False
|
||||
for wrapper in wrappers:
|
||||
if wrapper not in data.lower():
|
||||
continue
|
||||
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
return data
|
||||
|
||||
# Do stuff here.
|
||||
innervalue = parse_nested_param(data, maxDepth(data)-0)
|
||||
outervalue = parse_nested_param(data, maxDepth(data)-1)
|
||||
print("INNER: ", outervalue)
|
||||
print("OUTER: ", outervalue)
|
||||
|
||||
if outervalue != innervalue:
|
||||
#print("Outer: ", outervalue, " inner: ", innervalue)
|
||||
for key in range(len(innervalue)):
|
||||
# Replace OUTERVALUE[key] with INNERVALUE[key] in data.
|
||||
print("Replace %s with %s in %s" % (outervalue[key], innervalue[key], data))
|
||||
data = data.replace(outervalue[key], innervalue[key])
|
||||
else:
|
||||
for thistype in wrappers:
|
||||
if thistype.lower() not in data.lower():
|
||||
continue
|
||||
|
||||
parsed_value = parse_type(innervalue[0], thistype.lower())
|
||||
return parsed_value
|
||||
|
||||
print("DATA: %s\n" % data)
|
||||
return parse_wrapper(data)
|
||||
|
||||
def parse_wrapper_start(data):
|
||||
newdata = []
|
||||
newstring = ""
|
||||
record = True
|
||||
paranCnt = 0
|
||||
for char in data:
|
||||
if char == "(":
|
||||
paranCnt += 1
|
||||
|
||||
if not record:
|
||||
record = True
|
||||
|
||||
if record:
|
||||
newstring += char
|
||||
|
||||
if paranCnt == 0 and char == " ":
|
||||
newdata.append(newstring)
|
||||
newstring = ""
|
||||
record = True
|
||||
|
||||
if char == ")":
|
||||
paranCnt -= 1
|
||||
|
||||
if paranCnt == 0:
|
||||
record = False
|
||||
|
||||
if len(newstring) > 0:
|
||||
newdata.append(newstring)
|
||||
|
||||
print(newdata)
|
||||
parsedlist = []
|
||||
non_string = False
|
||||
for item in newdata:
|
||||
ret = parse_wrapper(item)
|
||||
if not isinstance(ret, str):
|
||||
non_string = True
|
||||
|
||||
parsedlist.append(ret)
|
||||
|
||||
if len(parsedlist) > 0 and not non_string:
|
||||
return " ".join(parsedlist)
|
||||
elif len(parsedlist) == 1 and non_string:
|
||||
return parsedlist[0]
|
||||
else:
|
||||
print("Casting back to string because multi: ", parsedlist)
|
||||
newlist = []
|
||||
for item in parsedlist:
|
||||
try:
|
||||
newlist.append(str(item))
|
||||
except ValueError:
|
||||
newlist.append("parsing_error")
|
||||
return " ".join(newlist)
|
||||
|
||||
# Takes a workflow execution as argument
|
||||
# Returns a string if the result is single, or a list if it's a list
|
||||
def get_json_value(execution_data, input_data):
|
||||
@@ -124,13 +327,36 @@ class AppBase:
|
||||
|
||||
print("BEFORE VARIABLES!")
|
||||
if len(baseresult) == 0:
|
||||
print("Variables: %s" % execution_data["workflow"]["workflow_variables"])
|
||||
for variable in execution_data["workflow"]["workflow_variables"]:
|
||||
variablename = variable["name"].replace(" ", "_", -1).lower()
|
||||
try:
|
||||
#print("WF Variables: %s" % execution_data["workflow"]["workflow_variables"])
|
||||
for variable in execution_data["workflow"]["workflow_variables"]:
|
||||
variablename = variable["name"].replace(" ", "_", -1).lower()
|
||||
|
||||
if variablename.lower() == actionname_lower:
|
||||
baseresult = variable["value"]
|
||||
break
|
||||
if variablename.lower() == actionname_lower:
|
||||
baseresult = variable["value"]
|
||||
break
|
||||
except KeyError as e:
|
||||
print("KeyError wf variables: %s" % e)
|
||||
pass
|
||||
except TypeError as e:
|
||||
print("TypeError wf variables: %s" % e)
|
||||
pass
|
||||
|
||||
print("BEFORE EXECUTION VAR")
|
||||
if len(baseresult) == 0:
|
||||
try:
|
||||
#print("Execution Variables: %s" % execution_data["execution_variables"])
|
||||
for variable in execution_data["execution_variables"]:
|
||||
variablename = variable["name"].replace(" ", "_", -1).lower()
|
||||
if variablename.lower() == actionname_lower:
|
||||
baseresult = variable["value"]
|
||||
break
|
||||
except KeyError as e:
|
||||
print("KeyError exec variables: %s" % e)
|
||||
pass
|
||||
except TypeError as e:
|
||||
print("TypeError exec variables: %s" % e)
|
||||
pass
|
||||
|
||||
except KeyError as error:
|
||||
print(f"KeyError in JSON: {error}")
|
||||
@@ -185,9 +411,11 @@ class AppBase:
|
||||
|
||||
return basejson
|
||||
|
||||
|
||||
def parse_params(action, fullexecution, parameter):
|
||||
# Skip if it starts with $?
|
||||
jsonparsevalue = "$."
|
||||
match = ".*([$]{1}([a-zA-Z0-9()# _-]+\.?){1,})"
|
||||
match = ".*([$]{1}([a-zA-Z0-9# _-]+\.?){1,})"
|
||||
|
||||
# Regex to find all the things
|
||||
if parameter["variant"] == "STATIC_VALUE":
|
||||
@@ -196,6 +424,7 @@ class AppBase:
|
||||
#self.logger.debug(f"\n\nHandle static data with JSON: {data}\n\n")
|
||||
#self.logger.info("STATIC PARSED: %s" % actualitem)
|
||||
if len(actualitem) > 0:
|
||||
print("ACTUAL: %s", actualitem)
|
||||
for replace in actualitem:
|
||||
try:
|
||||
to_be_replaced = replace[0]
|
||||
@@ -216,10 +445,33 @@ class AppBase:
|
||||
|
||||
|
||||
if parameter["variant"] == "WORKFLOW_VARIABLE":
|
||||
for item in fullexecution["workflow"]["workflow_variables"]:
|
||||
if parameter["action_field"] == item["name"]:
|
||||
parameter["value"] = item["value"]
|
||||
break
|
||||
print("Handling workflow variable")
|
||||
found = False
|
||||
try:
|
||||
for item in fullexecution["workflow"]["workflow_variables"]:
|
||||
if parameter["action_field"] == item["name"]:
|
||||
found = True
|
||||
parameter["value"] = item["value"]
|
||||
break
|
||||
except KeyError as e:
|
||||
print("KeyError WF variable 1: %s" % e)
|
||||
pass
|
||||
except TypeError as e:
|
||||
print("TypeError WF variables 1: %s" % e)
|
||||
pass
|
||||
|
||||
if not found:
|
||||
try:
|
||||
for item in fullexecution["execution_variables"]:
|
||||
if parameter["action_field"] == item["name"]:
|
||||
parameter["value"] = item["value"]
|
||||
break
|
||||
except KeyError as e:
|
||||
print("KeyError WF variable 2: %s" % e)
|
||||
pass
|
||||
except TypeError as e:
|
||||
print("TypeError WF variables 2: %s" % e)
|
||||
pass
|
||||
|
||||
elif parameter["variant"] == "ACTION_RESULT":
|
||||
# FIXME - calculate value based on action_field and $if prominent
|
||||
@@ -229,9 +481,7 @@ class AppBase:
|
||||
tmpvalue = ""
|
||||
self.logger.info("ACTION FIELD: %s" % parameter["action_field"])
|
||||
|
||||
#"$%s%s" %
|
||||
fullname = "$"
|
||||
|
||||
if parameter["action_field"] == "Execution Argument":
|
||||
tmpvalue = fullexecution["execution_argument"]
|
||||
fullname += "exec"
|
||||
@@ -240,8 +490,8 @@ class AppBase:
|
||||
|
||||
if parameter["value"].startswith(jsonparsevalue):
|
||||
fullname += parameter["value"][2:]
|
||||
else:
|
||||
fullname = "$%s" % parameter["action_field"]
|
||||
#else:
|
||||
# fullname = "$%s" % parameter["action_field"]
|
||||
|
||||
self.logger.info("Fullname: %s" % fullname)
|
||||
actualitem = re.findall(match, fullname, re.MULTILINE)
|
||||
@@ -331,7 +581,9 @@ class AppBase:
|
||||
if check:
|
||||
return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)
|
||||
|
||||
print(sourcevalue)
|
||||
|
||||
#sourcevalue = sourcevalue.encode("utf-8")
|
||||
sourcevalue = parse_wrapper_start(sourcevalue)
|
||||
destinationvalue = condition["destination"]["value"]
|
||||
|
||||
if condition["destination"]["variant"]== "" or condition["destination"]["variant"]== "STATIC_VALUE":
|
||||
@@ -341,6 +593,8 @@ class AppBase:
|
||||
if check:
|
||||
return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)
|
||||
|
||||
#destinationvalue = destinationvalue.encode("utf-8")
|
||||
destinationvalue = parse_wrapper_start(destinationvalue)
|
||||
available_checks = [
|
||||
"=",
|
||||
"equals",
|
||||
@@ -442,7 +696,7 @@ class AppBase:
|
||||
for parameter in action["parameters"]:
|
||||
check, value = parse_params(action, fullexecution, parameter)
|
||||
if check:
|
||||
raise Exception(check)
|
||||
raise "Value check error: %s" % Exception(check)
|
||||
|
||||
# Custom format for ${name[0,1,2,...]}$
|
||||
submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})"
|
||||
@@ -483,17 +737,29 @@ class AppBase:
|
||||
# With this parameter ready, add it to... a greater list of parameters. Rofl
|
||||
multi_parameters[parameter["name"]] = resultarray
|
||||
else:
|
||||
# Parses things like int(value)
|
||||
self.logger.info("Parsing wrapper data")
|
||||
value = parse_wrapper_start(value)
|
||||
|
||||
params[parameter["name"]] = value
|
||||
multi_parameters[parameter["name"]] = value
|
||||
|
||||
# FIXME - this is horrible, but works for now
|
||||
#for i in range(calltimes):
|
||||
if not multiexecution:
|
||||
print("Params: %s" % params)
|
||||
print("RUNNING NORMAL EXECUTION")
|
||||
result += await func(**params)
|
||||
print("APP_SDK DONE: Starting normal execution of function")
|
||||
newres = await func(**params)
|
||||
print("NEWRES: ", newres)
|
||||
if isinstance(newres, str):
|
||||
result += newres
|
||||
else:
|
||||
try:
|
||||
result += str(result)
|
||||
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:
|
||||
print("MULTI EXECUTION: ", multi_parameters)
|
||||
print("APP_SDK DONE: Starting multi execution with", multi_parameters)
|
||||
# 1. Use number of executions based on longest array
|
||||
# 2. Find the right value from the parsed multi_params
|
||||
|
||||
@@ -558,7 +824,7 @@ class AppBase:
|
||||
print(f"Failed to execute: {e}")
|
||||
self.logger.exception(f"Failed to execute {e}-{action['id']}")
|
||||
action_result["status"] = "FAILURE"
|
||||
action_result["result"] = "Exception: %s" % e
|
||||
action_result["result"] = "General exception: %s" % e
|
||||
|
||||
action_result["completed_at"] = int(time.time())
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#!/bin/bash
|
||||
docker rmi frikky/shuffle:app_sdk
|
||||
docker build . -t frikky/shuffle:app_sdk --no-cache
|
||||
docker push frikky/shuffle:app_sdk
|
||||
NAME=app_sdk
|
||||
VERSION=0.2.0
|
||||
|
||||
docker rmi frikky/shuffle:$NAME --force
|
||||
docker build . -t frikky/shuffle:$NAME -t frikky/$NAME:$VERSION
|
||||
|
||||
docker push frikky/shuffle:$NAME
|
||||
docker push frikky/$NAME:$VERSION
|
||||
|
||||
@@ -339,14 +339,7 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet
|
||||
bodyAddin = ", data=body"
|
||||
|
||||
// FIXME: Does JSON data work?
|
||||
bodyFormatter = `
|
||||
if (body.startswith("{") and body.endswith("}")) or (body.startswith("[") and body.endswith("]")):
|
||||
try:
|
||||
body = json.dumps(body)
|
||||
except:
|
||||
pass
|
||||
`
|
||||
break
|
||||
bodyFormatter = `body = " ".join(body.strip().split()).encode("utf-8")`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,13 +368,14 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet
|
||||
|
||||
// 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
|
||||
url=f"%s%s"
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
return requests.%s(url, headers=headers%s%s%s).text
|
||||
`,
|
||||
functionname,
|
||||
@@ -505,7 +499,7 @@ func generateYaml(swagger *openapi3.Swagger, newmd5 string) (*openapi3.Swagger,
|
||||
Description: "The apikey to use",
|
||||
Multiline: false,
|
||||
Required: true,
|
||||
Example: "The API key to use. Space = skip",
|
||||
Example: "**********",
|
||||
Schema: SchemaDefinition{
|
||||
Type: "string",
|
||||
},
|
||||
@@ -531,7 +525,7 @@ func generateYaml(swagger *openapi3.Swagger, newmd5 string) (*openapi3.Swagger,
|
||||
Description: "The password to use",
|
||||
Multiline: false,
|
||||
Required: true,
|
||||
Example: "The password to use",
|
||||
Example: "***********",
|
||||
Schema: SchemaDefinition{
|
||||
Type: "string",
|
||||
},
|
||||
|
||||
@@ -189,17 +189,17 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin
|
||||
dockerFileTarReader,
|
||||
buildOptions,
|
||||
)
|
||||
//log.Printf("IMAGERESPONSE: %#v", imageBuildResponse.Body)
|
||||
|
||||
log.Printf("IMAGERESPONSE: %#v", imageBuildResponse.Body)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
defer imageBuildResponse.Body.Close()
|
||||
_, newerr := io.Copy(os.Stdout, imageBuildResponse.Body)
|
||||
if newerr != nil {
|
||||
log.Printf("Failed reading Docker build STDOUT: %s", newerr)
|
||||
}
|
||||
|
||||
// Read the STDOUT from the build process
|
||||
defer imageBuildResponse.Body.Close()
|
||||
_, err = io.Copy(os.Stdout, imageBuildResponse.Body)
|
||||
if err != nil {
|
||||
// Read the STDOUT from the build process
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+299
-154
@@ -70,7 +70,7 @@ var baseDockerName = "frikky/shuffle"
|
||||
var dbclient *datastore.Client
|
||||
|
||||
type Userapi struct {
|
||||
Username string `datastore:"Username"`
|
||||
Username string `datastore:"username"`
|
||||
ApiKey string `datastore:"apikey"`
|
||||
}
|
||||
|
||||
@@ -155,32 +155,35 @@ type Environment struct {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string `datastore:"Username"`
|
||||
Password string `datastore:"password,noindex"`
|
||||
Session string `datastore:"session,noindex"`
|
||||
Verified bool `datastore:"verified,noindex"`
|
||||
PrivateApps []WorkflowApp `datastore:"privateapps"`
|
||||
Role string `datastore:"role"`
|
||||
VerificationToken string `datastore:"verification_token"`
|
||||
ApiKey string `datastore:"apikey"`
|
||||
ResetReference string `datastore:"reset_reference"`
|
||||
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"`
|
||||
ResetTimeout int64 `datastore:"reset_timeout,noindex" json:"reset_timeout"`
|
||||
Id string `datastore:"id" json:"id"`
|
||||
Orgs string `datastore:"orgs" json:"orgs"`
|
||||
Orgs []string `datastore:"orgs" json:"orgs"`
|
||||
CreationTime int64 `datastore:"creation_time" json:"creation_time"`
|
||||
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"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -239,9 +242,11 @@ type AppInfo struct {
|
||||
// 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"`
|
||||
@@ -709,9 +714,9 @@ func handleApiAuthentication(resp http.ResponseWriter, request *http.Request) (U
|
||||
|
||||
// Get session first
|
||||
// Should basically never happen
|
||||
Userdata, err := getUser(ctx, session.Username)
|
||||
Userdata, err := getUser(ctx, session.Id)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", session.Username, err)
|
||||
log.Printf("Username %s doesn't exist (authcheck): %s", session.Username, err)
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
@@ -807,9 +812,9 @@ func parseLoginParameters(resp http.ResponseWriter, request *http.Request) (logi
|
||||
// Removed for localhost
|
||||
func checkPasswordStrength(password string) error {
|
||||
// Check password strength here
|
||||
//if len(password) < 10 {
|
||||
// return errors.New("Minimum password length is 10.")
|
||||
//}
|
||||
if len(password) < 10 {
|
||||
return errors.New("Minimum password length is 10.")
|
||||
}
|
||||
|
||||
//if len(password) > 128 {
|
||||
// return errors.New("Maximum password length is 128.")
|
||||
@@ -879,8 +884,8 @@ func handleRegisterVerification(resp http.ResponseWriter, request *http.Request)
|
||||
_, err := dbclient.GetAll(ctx, q, &users)
|
||||
if err != nil {
|
||||
log.Printf("Failed getting users for verification token: %s", err)
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "%s"}`, defaultMessage)))
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, defaultMessage)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1008,11 +1013,12 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("User role: %s", user.Role)
|
||||
if err == nil && user.Role != "admin" && count > 0 {
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Can't register without being admin (2)"}`))
|
||||
return
|
||||
if count != 0 {
|
||||
if user.Role != "admin" {
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Can't register without being admin (2)"}`))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a struct of Username, password
|
||||
@@ -1042,10 +1048,18 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME - use it somehow
|
||||
ctx := context.Background()
|
||||
_, err = getUser(ctx, data.Username)
|
||||
if err == nil {
|
||||
q := datastore.NewQuery("Users").Filter("Username =", data.Username)
|
||||
var users []User
|
||||
_, err = dbclient.GetAll(ctx, q, &users)
|
||||
if err != nil {
|
||||
log.Printf("Failed getting user for registration: %s", err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting username"}`)))
|
||||
return
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
log.Printf("Username %s exists and can't register", data.Username)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`))
|
||||
@@ -1064,14 +1078,20 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) {
|
||||
newUser.Username = data.Username
|
||||
newUser.Password = string(hashedPassword)
|
||||
newUser.Verified = false
|
||||
newUser.Role = "user"
|
||||
newUser.CreationTime = time.Now().Unix()
|
||||
newUser.Active = true
|
||||
newUser.Orgs = []string{"default"}
|
||||
|
||||
// FIXME - Remove this later
|
||||
newUser.Role = "admin"
|
||||
if count == 0 {
|
||||
newUser.Role = "admin"
|
||||
newUser.Roles = []string{"admin"}
|
||||
} else {
|
||||
newUser.Role = "user"
|
||||
newUser.Roles = []string{"user"}
|
||||
}
|
||||
|
||||
// set limits
|
||||
// WorkflowExecutions > CloudExecutions simply because of onprem
|
||||
newUser.Limits.DailyApiUsage = 100
|
||||
newUser.Limits.DailyWorkflowExecutions = 1000
|
||||
newUser.Limits.DailyCloudExecutions = 100
|
||||
@@ -1124,35 +1144,14 @@ Registration URL :)
|
||||
log.Printf("Couldn't send email: %v", err)
|
||||
}
|
||||
|
||||
//sessionToken := uuid.NewV4()
|
||||
|
||||
//// Finally, we set the client cookie for "session_token" as the session token we just generated
|
||||
//// we also set an expiry time of 120 seconds, the same as the cache
|
||||
//http.SetCookie(resp, &http.Cookie{
|
||||
// Name: "session_token",
|
||||
// Value: sessionToken.String(),
|
||||
// Expires: time.Now().Add(1200 * time.Second),
|
||||
//})
|
||||
|
||||
//log.Println(Userdata)
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(`{"success": true}`))
|
||||
log.Printf("%s Successfully registered.", data.Username)
|
||||
|
||||
//err = SetSession(*newUser, sessionToken.String())
|
||||
//if err != nil {
|
||||
// log.Printf("Error adding session to database: %s", err)
|
||||
//}
|
||||
|
||||
//err = SetApikey(*newUser)
|
||||
//if err != nil {
|
||||
// log.Printf("Error adding apikey to database: %s", err)
|
||||
//}
|
||||
|
||||
//err = SetSession(*newUser, sessionToken.String())
|
||||
//if err != nil {
|
||||
// log.Printf("Error adding apikey to database: %s", err)
|
||||
//}
|
||||
err = increaseStatisticsField(ctx, "successful_register", data.Username, 1)
|
||||
if err != nil {
|
||||
log.Printf("Failed to increase total apps loaded stats: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleCookie(request *http.Request) bool {
|
||||
@@ -1212,9 +1211,9 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
// Get session first
|
||||
// Should basically never happen
|
||||
_, err = getUser(ctx, session.Username)
|
||||
_, err = getUser(ctx, session.Id)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", session.Username, err)
|
||||
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
|
||||
@@ -1306,9 +1305,9 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
// Get session first
|
||||
// Should basically never happen
|
||||
userInfo, err := getUser(ctx, session.Username)
|
||||
userInfo, err := getUser(ctx, session.Id)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", session.Username, err)
|
||||
log.Printf("Username %s doesn't exist (apigen): %s", session.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": ""}`))
|
||||
return
|
||||
@@ -1337,7 +1336,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
log.Printf("Updated apikey for user %s", userInfo.Username)
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "Username": "%s", "verified": %t, "apikey": "%s"}`, userInfo.Username, userInfo.Verified, userInfo.ApiKey)))
|
||||
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) {
|
||||
@@ -1366,9 +1365,9 @@ func handleSettings(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
// Get session first
|
||||
// Should basically never happen
|
||||
UserInfo, err := getUser(ctx, session.Username)
|
||||
UserInfo, err := getUser(ctx, session.Id)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", session.Username, err)
|
||||
log.Printf("Username %s doesn't exist (settings): %s", session.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": ""}`))
|
||||
return
|
||||
@@ -1383,7 +1382,7 @@ func handleSettings(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "Username": "%s", "verified": %t, "apikey": "%s"}`, UserInfo.Username, UserInfo.Verified, UserInfo.ApiKey)))
|
||||
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) {
|
||||
@@ -1405,26 +1404,80 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
ctx := context.Background()
|
||||
|
||||
sessionToken := c.Value
|
||||
//log.Printf("Found session %s", sessionToken)
|
||||
session, err := getSession(ctx, sessionToken)
|
||||
if err != nil {
|
||||
//log.Printf("Session %#v doesn't exist: %s", session, err)
|
||||
log.Printf("Session %#v doesn't exist: %s", session, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": ""}`))
|
||||
resp.Write([]byte(`{"success": false, "reason": "No session"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Get session first
|
||||
// Should basically never happen
|
||||
UserInfo, err := getUser(ctx, session.Username)
|
||||
userInfo, err := getUser(ctx, session.Id)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", session.Username, err)
|
||||
log.Printf("Username %s doesn't exist (info): %s", session.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": ""}`))
|
||||
return
|
||||
}
|
||||
|
||||
// This is a long check to see if an inactive admin can access the portal
|
||||
if !userInfo.Active {
|
||||
if userInfo.Role == "admin" {
|
||||
ctx := context.Background()
|
||||
q := datastore.NewQuery("Users")
|
||||
var users []User
|
||||
_, err = dbclient.GetAll(ctx, q, &users)
|
||||
if err != nil {
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Failed to get other users when verifying admin user"}`))
|
||||
return
|
||||
}
|
||||
|
||||
activeFound := false
|
||||
adminFound := false
|
||||
for _, user := range users {
|
||||
if user.Id == userInfo.Id {
|
||||
continue
|
||||
}
|
||||
|
||||
if user.Role != "admin" {
|
||||
continue
|
||||
}
|
||||
|
||||
if user.Active {
|
||||
activeFound = true
|
||||
}
|
||||
|
||||
adminFound = true
|
||||
}
|
||||
|
||||
// Must ALWAYS be an active admin
|
||||
// Will return no access if another admin is active
|
||||
if !adminFound {
|
||||
log.Printf("NO OTHER ADMINS FOUND - CONTINUE!")
|
||||
} else {
|
||||
//
|
||||
if activeFound {
|
||||
log.Printf("OTHER ACTIVE ADMINS FOUND - CAN'T PASS")
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "This user is locked"}`))
|
||||
return
|
||||
} else {
|
||||
log.Printf("NO OTHER ADMINS FOUND - CONTINUE!")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "This user is locked"}`))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("%s %s", session.Session, UserInfo.Session)
|
||||
if session.Session != UserInfo.Session {
|
||||
if session.Session != userInfo.Session {
|
||||
log.Printf("Session %s is not the latest. %s", session.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": ""}`))
|
||||
@@ -1434,11 +1487,11 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
expiration := time.Now().Add(3600 * time.Second)
|
||||
http.SetCookie(resp, &http.Cookie{
|
||||
Name: "session_token",
|
||||
Value: UserInfo.Session,
|
||||
Value: userInfo.Session,
|
||||
Expires: expiration,
|
||||
})
|
||||
|
||||
returnData := fmt.Sprintf(`{"success": true, "cookies": [{"key": "session_token", "value": "%s", "expiration": %d}]}`, UserInfo.Session, expiration.Unix())
|
||||
returnData := fmt.Sprintf(`{"success": true, "cookies": [{"key": "session_token", "value": "%s", "expiration": %d}]}`, userInfo.Session, expiration.Unix())
|
||||
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(returnData))
|
||||
@@ -1451,9 +1504,10 @@ type passwordReset struct {
|
||||
}
|
||||
|
||||
type passwordChange struct {
|
||||
Password1 string `json:"newpassword"`
|
||||
Password2 string `json:"newpassword2"`
|
||||
Password3 string `json:"currentpassword"`
|
||||
Username string `json:"username"`
|
||||
Newpassword string `json:"newpassword"`
|
||||
Newpassword2 string `json:"newpassword2"`
|
||||
Currentpassword string `json:"currentpassword"`
|
||||
}
|
||||
|
||||
func handlePasswordResetMail(resp http.ResponseWriter, request *http.Request) {
|
||||
@@ -1474,7 +1528,7 @@ func handlePasswordResetMail(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
type passwordReset struct {
|
||||
Username string `json:"Username"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
var t passwordReset
|
||||
@@ -1489,7 +1543,7 @@ func handlePasswordResetMail(resp http.ResponseWriter, request *http.Request) {
|
||||
ctx := context.Background()
|
||||
Userdata, err := getUser(ctx, t.Username)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", t.Username, err)
|
||||
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
|
||||
@@ -1589,8 +1643,8 @@ func handlePasswordReset(resp http.ResponseWriter, request *http.Request) {
|
||||
_, err = dbclient.GetAll(ctx, q, &users)
|
||||
if err != nil {
|
||||
log.Printf("Failed getting users: %s", err)
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "%s"}`, defaultMessage)))
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, defaultMessage)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1629,13 +1683,12 @@ func handlePasswordReset(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func handlePasswordChange(resp http.ResponseWriter, request *http.Request) {
|
||||
log.Println("Handling password change")
|
||||
|
||||
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")
|
||||
@@ -1644,6 +1697,7 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) {
|
||||
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 {
|
||||
@@ -1653,21 +1707,41 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if t.Password1 != t.Password2 {
|
||||
user, err := handleApiAuthentication(resp, request)
|
||||
if err != nil {
|
||||
log.Printf("Api authentication failed in set new workflowhandler: %s", err)
|
||||
resp.WriteHeader(401)
|
||||
err := "Passwords don't match"
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
||||
resp.Write([]byte(`{"success": false}`))
|
||||
return
|
||||
}
|
||||
|
||||
if len(t.Password1) < 10 || len(t.Password2) < 10 {
|
||||
curUserFound := false
|
||||
if t.Username != user.Username && user.Role != "admin" {
|
||||
resp.WriteHeader(401)
|
||||
err := "Passwords don't match - 2"
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
||||
resp.Write([]byte(`{"success": false, "reason": "Admin required to change others' passwords"}`))
|
||||
return
|
||||
} else if t.Username == user.Username {
|
||||
curUserFound = true
|
||||
}
|
||||
|
||||
err = checkPasswordStrength(t.Password3)
|
||||
if user.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
|
||||
}
|
||||
}
|
||||
|
||||
// Current password
|
||||
err = checkPasswordStrength(t.Newpassword)
|
||||
if err != nil {
|
||||
log.Printf("Bad password strength: %s", err)
|
||||
resp.WriteHeader(401)
|
||||
@@ -1675,56 +1749,52 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check cookie
|
||||
c, err := request.Cookie("session_token")
|
||||
if err != nil {
|
||||
log.Printf("User doesn't have sessiontoken on pw change: %s", err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "You're not logged in."}`)))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// Validate with User
|
||||
sessionToken := c.Value
|
||||
session, err := getSession(ctx, sessionToken)
|
||||
if err != nil {
|
||||
log.Printf("Session %s doesn't exist (password change): %s", session.Session, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "You're not logged in"}`))
|
||||
return
|
||||
if !curUserFound {
|
||||
log.Printf("Have to find a different user")
|
||||
q := datastore.NewQuery("Users").Filter("Username =", strings.ToLower(t.Username))
|
||||
var users []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 users with the same username: %s: %d`, t.Username, len(users))
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found multiple users with the same username: %s"}`, t.Username)))
|
||||
return
|
||||
}
|
||||
|
||||
user = users[0]
|
||||
} else {
|
||||
// Admins can re-generate others' passwords as well.
|
||||
if user.Role != "admin" {
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(t.Newpassword))
|
||||
if err != nil {
|
||||
log.Printf("Bad password for %s: %s", user.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get session first
|
||||
// Should basically never happen
|
||||
Userdata, err := getUser(ctx, session.Username)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(t.Newpassword), 8)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", session.Username, err)
|
||||
log.Printf("New password failure for %s: %s", user.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`))
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(Userdata.Password), []byte(t.Password1))
|
||||
user.Password = string(hashedPassword)
|
||||
err = setUser(ctx, &user)
|
||||
if err != nil {
|
||||
log.Printf("Bad password for %s: %s", session.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`))
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(t.Password3), 8)
|
||||
if err != nil {
|
||||
log.Printf("Wrong password for %s: %s", Userdata.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`))
|
||||
return
|
||||
}
|
||||
|
||||
Userdata.Password = string(hashedPassword)
|
||||
err = setUser(ctx, Userdata)
|
||||
if err != nil {
|
||||
log.Printf("Error adding User %s: %s", Userdata.Username, err)
|
||||
log.Printf("Error fixing password for user %s: %s", user.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`))
|
||||
return
|
||||
@@ -1942,12 +2012,10 @@ func handleGetUsers(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
log.Printf("HELLO?")
|
||||
cors := handleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
log.Printf("HELLO2?")
|
||||
|
||||
count, err := getUserCount()
|
||||
if err != nil {
|
||||
@@ -1991,14 +2059,25 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
Userdata, err := getUser(ctx, data.Username)
|
||||
q := datastore.NewQuery("Users").Filter("Username =", strings.ToLower(data.Username))
|
||||
var users []User
|
||||
_, err = dbclient.GetAll(ctx, q, &users)
|
||||
if err != nil {
|
||||
log.Printf("Username %s doesn't exist: %s", data.Username, err)
|
||||
log.Printf("Failed getting user %s", data.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 users with the same username: %s: %d`, data.Username, len(users))
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found multiple users with the same username: %s"}`, data.Username)))
|
||||
return
|
||||
}
|
||||
|
||||
Userdata := users[0]
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(Userdata.Password), []byte(data.Password))
|
||||
if err != nil {
|
||||
log.Printf("Password for %s is incorrect: %s", data.Username, err)
|
||||
@@ -2007,7 +2086,14 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("%s SUCCESSFULLY LOGGED IN", data.Username)
|
||||
if !Userdata.Active {
|
||||
log.Printf("%s is not active, but tried to login", data.Username, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "This user is deactivated"}`))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("%s SUCCESSFULLY LOGGED IN with session %s", data.Username, Userdata.Session)
|
||||
//if !Userdata.Verified {
|
||||
// log.Printf("User %s is not verified", data.Username)
|
||||
// resp.WriteHeader(403)
|
||||
@@ -2031,7 +2117,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 = SetSession(ctx, Userdata, Userdata.Session)
|
||||
if err != nil {
|
||||
log.Printf("Error adding session to database: %s", err)
|
||||
}
|
||||
@@ -2042,7 +2128,6 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
sessionToken := uuid.NewV4()
|
||||
|
||||
http.SetCookie(resp, &http.Cookie{
|
||||
Name: "session_token",
|
||||
Value: sessionToken.String(),
|
||||
@@ -2050,7 +2135,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
})
|
||||
|
||||
// ADD TO DATABASE
|
||||
err = SetSession(ctx, *Userdata, sessionToken.String())
|
||||
err = SetSession(ctx, Userdata, sessionToken.String())
|
||||
if err != nil {
|
||||
log.Printf("Error adding session to database: %s", err)
|
||||
}
|
||||
@@ -2065,7 +2150,7 @@ func getApikey(ctx context.Context, apikey string) (User, error) {
|
||||
var users []User
|
||||
_, err := dbclient.GetAll(ctx, q, &users)
|
||||
if err != nil {
|
||||
log.Printf("Error getting users apikey: %s", err)
|
||||
log.Printf("Error getting users apikey (getapikey): %s", err)
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
@@ -2088,8 +2173,8 @@ func getSession(ctx context.Context, thissession string) (*session, error) {
|
||||
}
|
||||
|
||||
// ListBooks returns a list of books, ordered by title.
|
||||
func getUser(ctx context.Context, Username string) (*User, error) {
|
||||
key := datastore.NameKey("Users", strings.ToLower(Username), 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
|
||||
@@ -2133,7 +2218,7 @@ func SetApikey(ctx context.Context, Userdata User) error {
|
||||
func SetSession(ctx context.Context, Userdata User, value string) error {
|
||||
// Non indexed User data
|
||||
Userdata.Session = value
|
||||
key1 := datastore.NameKey("Users", strings.ToLower(Userdata.Username), nil)
|
||||
key1 := datastore.NameKey("Users", Userdata.Id, nil)
|
||||
|
||||
// New struct, to not add body, author etc
|
||||
if _, err := dbclient.Put(ctx, key1, &Userdata); err != nil {
|
||||
@@ -2146,6 +2231,7 @@ func SetSession(ctx context.Context, Userdata User, value string) error {
|
||||
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 {
|
||||
@@ -2193,10 +2279,7 @@ func setEnvironment(ctx context.Context, data *Environment) error {
|
||||
// ListBooks returns a list of books, ordered by title.
|
||||
func setUser(ctx context.Context, data *User) error {
|
||||
// clear session_token and API_token for user
|
||||
k := datastore.NameKey("Users", strings.ToLower(data.Username), nil)
|
||||
|
||||
// New struct, to not add body, author etc
|
||||
|
||||
k := datastore.NameKey("Users", data.Id, nil)
|
||||
if _, err := dbclient.Put(ctx, k, data); err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
@@ -5852,7 +5935,7 @@ func handleAppHotload(location string) error {
|
||||
fs, err := createFs(basepath, location)
|
||||
if err != nil {
|
||||
log.Printf("Failed memfs creation - probably bad path: %s", err)
|
||||
return err
|
||||
return errors.New(fmt.Sprintf("Failed to find directory %s", location))
|
||||
} else {
|
||||
log.Printf("Memfs creation from %s done", location)
|
||||
}
|
||||
@@ -5863,7 +5946,7 @@ func handleAppHotload(location string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Reading app folder: %#v", dir)
|
||||
//log.Printf("Reading app folder: %#v", dir)
|
||||
err = iterateAppGithubFolders(fs, dir, "", "")
|
||||
if err != nil {
|
||||
log.Printf("Err: %s", err)
|
||||
@@ -5876,12 +5959,58 @@ func handleAppHotload(location string) error {
|
||||
// Handles configuration items during Shuffle startup
|
||||
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)
|
||||
if err != nil {
|
||||
log.Printf("Failed increasing local stats: %s", err)
|
||||
}
|
||||
|
||||
// Fix active users etc
|
||||
log.Printf("Reformatting users")
|
||||
q := datastore.NewQuery("Users").Filter("active =", true)
|
||||
var users []User
|
||||
_, err = dbclient.GetAll(ctx, q, &users)
|
||||
if err != nil {
|
||||
log.Printf("Error getting users apikey (runinit): %s", err)
|
||||
} else {
|
||||
if len(users) == 0 {
|
||||
log.Printf("No active users found - setting ALL to active")
|
||||
q := datastore.NewQuery("Users")
|
||||
var users []User
|
||||
_, err := dbclient.GetAll(ctx, q, &users)
|
||||
if err == nil {
|
||||
for _, user := range users {
|
||||
user.Active = true
|
||||
if len(user.Username) == 0 {
|
||||
DeleteKey(ctx, "Users", strings.ToLower(user.Username))
|
||||
continue
|
||||
}
|
||||
|
||||
if len(user.Role) > 0 {
|
||||
user.Roles = append(user.Roles, user.Role)
|
||||
}
|
||||
|
||||
if len(user.Orgs) == 0 {
|
||||
user.Orgs = []string{"default"}
|
||||
}
|
||||
|
||||
err = setUser(ctx, &user)
|
||||
if err != nil {
|
||||
log.Printf("Failed to reset user")
|
||||
} else {
|
||||
log.Printf("Remade user %s with ID", user.Id)
|
||||
err = DeleteKey(ctx, "Users", strings.ToLower(user.Username))
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete old user by username")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gets environments and inits if it doesn't exist
|
||||
log.Printf("Setting up environments")
|
||||
count, err := getEnvironmentCount()
|
||||
if count == 0 && err == nil {
|
||||
item := Environment{
|
||||
@@ -5896,16 +6025,18 @@ func runInit(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Gets schedules and starts them
|
||||
log.Printf("Relaunching schedules")
|
||||
schedules, err := getAllSchedules(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed getting schedules during service init: %s", err)
|
||||
} else {
|
||||
log.Printf("Setting up %d schedule(s)", len(schedules))
|
||||
for _, schedule := range schedules {
|
||||
//log.Printf("Schedule: %#v", schedule)
|
||||
job := func() {
|
||||
request := &http.Request{
|
||||
Method: "POST",
|
||||
Body: ioutil.NopCloser(strings.NewReader(schedule.Argument)),
|
||||
Body: ioutil.NopCloser(strings.NewReader(schedule.WrappedArgument)),
|
||||
}
|
||||
|
||||
_, _, err := handleExecution(schedule.WorkflowId, Workflow{}, request)
|
||||
@@ -5925,6 +6056,7 @@ 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)
|
||||
if err != nil {
|
||||
log.Printf("Failed getting apps: %s", err)
|
||||
@@ -6006,11 +6138,13 @@ func init() {
|
||||
var err error
|
||||
ctx := context.Background()
|
||||
|
||||
log.Printf("Running INIT process")
|
||||
log.Printf("Starting Shuffle backend - initializing database connection")
|
||||
// option.WithoutAuthentication
|
||||
dbclient, err = datastore.NewClient(ctx, gceProject)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("DBclient error during init: %s", err))
|
||||
}
|
||||
log.Printf("Finished Shuffle database init")
|
||||
|
||||
go runInit(ctx)
|
||||
|
||||
@@ -6022,27 +6156,37 @@ func init() {
|
||||
r.HandleFunc("/functions/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/functions/outlook/getFolders", handleGetOutlookFolders).Methods("GET", "OPTIONS")
|
||||
|
||||
// General
|
||||
// Make user related locations
|
||||
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/getusers", handleGetUsers).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/generateapikey", handleApiGeneration).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/users/{user}", deleteUser).Methods("DELETE", "OPTIONS")
|
||||
|
||||
// General - duplicates and old.
|
||||
r.HandleFunc("/api/v1/login", handleLogin).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/logout", 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/getenvironments", handleGetEnvironments).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/setenvironments", handleSetEnvironments).Methods("PUT", "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", "OPTIONS")
|
||||
|
||||
r.HandleFunc("/api/v1/getenvironments", handleGetEnvironments).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/setenvironments", handleSetEnvironments).Methods("PUT", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/passwordchange", handlePasswordChange).Methods("POST", "OPTIONS")
|
||||
|
||||
r.HandleFunc("/api/v1/docs", getDocList).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/docs/{key}", getDocs).Methods("GET", "OPTIONS")
|
||||
|
||||
// Queuebuilder and Workflow streams. First is to update a stream, second to get a stream
|
||||
// Changed from workflows/streams to streams, as appengine was messing up
|
||||
// This does not increase the API counter
|
||||
r.HandleFunc("/api/v1/workflows/queue", handleGetWorkflowqueue).Methods("GET")
|
||||
r.HandleFunc("/api/v1/workflows/queue/confirm", handleGetWorkflowqueueConfirm).Methods("POST")
|
||||
r.HandleFunc("/api/v1/streams", handleWorkflowQueue).Methods("POST")
|
||||
r.HandleFunc("/api/v1/streams/results", handleGetStreamResults).Methods("POST", "OPTIONS")
|
||||
|
||||
@@ -6067,9 +6211,10 @@ func init() {
|
||||
/* 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/queue", handleGetWorkflowqueue).Methods("GET")
|
||||
r.HandleFunc("/api/v1/workflows/queue/confirm", handleGetWorkflowqueueConfirm).Methods("POST")
|
||||
r.HandleFunc("/api/v1/workflows/schedules", handleGetSchedules).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/workflows/download_remote", loadSpecificWorkflows).Methods("POST", "OPTIONS")
|
||||
//r.HandleFunc("/api/v1/workflows/{key}/execute_fs", executeWorkflowFS)
|
||||
r.HandleFunc("/api/v1/workflows/{key}/execute", executeWorkflow).Methods("GET", "POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/workflows/{key}/schedule/{schedule}", stopSchedule).Methods("DELETE", "OPTIONS")
|
||||
|
||||
+585
-187
File diff suppressed because one or more lines are too long
@@ -30,6 +30,7 @@ services:
|
||||
- ORG_ID=${ORG_ID}
|
||||
- DATASTORE_EMULATOR_HOST=shuffle-database:8000
|
||||
- APP_DOWNLOAD_LOCATION=${APP_DOWNLOAD_LOCATION}
|
||||
- APP_HOTLOAD_FOLDER=/shuffle-apps
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
Generated
+8137
-11825
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "shuffler",
|
||||
"homepage": "https://shuffler.io",
|
||||
"version": "0.3.0",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.5.2",
|
||||
@@ -49,7 +49,7 @@
|
||||
"react-powerhooks": "0.0.7",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-scripts": "^2.1.8",
|
||||
"react-scripts": "^3.4.1",
|
||||
"reactstrap": "^7.1.0",
|
||||
"shellwords": "^0.1.1",
|
||||
"simplebar": "^4.2.3",
|
||||
|
||||
+222
-6
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect} from 'react';
|
||||
|
||||
import {Link} from 'react-router-dom';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import List from '@material-ui/core/List';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
@@ -31,6 +32,9 @@ const Admin = (props) => {
|
||||
const [users, setUsers] = React.useState([]);
|
||||
const [environments, setEnvironments] = React.useState([]);
|
||||
const [schedules, setSchedules] = React.useState([])
|
||||
const [selectedUser, setSelectedUser] = React.useState({})
|
||||
const [newPassword, setNewPassword] = React.useState("");
|
||||
const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false)
|
||||
|
||||
const alert = useAlert()
|
||||
|
||||
@@ -39,7 +43,7 @@ const Admin = (props) => {
|
||||
console.log("INPUT: ", data)
|
||||
|
||||
// Just use this one?
|
||||
const url = globalUrl+'/api/v1/workflows/'+data["workflow_id datastore:"]+"/schedule/"+data.id
|
||||
const url = globalUrl+'/api/v1/workflows/'+data["workflow_id"]+"/schedule/"+data.id
|
||||
console.log("URL: ", url)
|
||||
fetch(url, {
|
||||
method: 'DELETE',
|
||||
@@ -50,6 +54,7 @@ const Admin = (props) => {
|
||||
})
|
||||
.then(response =>
|
||||
response.json().then(responseJson => {
|
||||
console.log("RESP: ", responseJson)
|
||||
if (responseJson["success"] === false) {
|
||||
alert.error("Failed stopping schedule")
|
||||
} else {
|
||||
@@ -63,6 +68,64 @@ const Admin = (props) => {
|
||||
});
|
||||
}
|
||||
|
||||
const onPasswordChange = () => {
|
||||
const data = {"username": selectedUser.username, "newpassword": newPassword}
|
||||
const url = globalUrl+'/api/v1/passwordchange';
|
||||
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 setting new password")
|
||||
} else {
|
||||
alert.success("Changed password!")
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch(error => {
|
||||
alert.error("Err: ", error.toString())
|
||||
});
|
||||
}
|
||||
|
||||
const deleteUser = (data) => {
|
||||
// Just use this one?
|
||||
const url = globalUrl+'/api/v1/users/'+data.id
|
||||
fetch(url, {
|
||||
method: 'DELETE',
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
getUsers()
|
||||
}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success && responseJson.reason !== undefined) {
|
||||
alert.error("Failed to deactivate user: "+responseJson.reason)
|
||||
} else {
|
||||
alert.success("Deactivated user "+data.id)
|
||||
}
|
||||
})
|
||||
|
||||
.catch(error => {
|
||||
console.log("Error in userdata: ", error)
|
||||
});
|
||||
}
|
||||
|
||||
const submitUser = (data) => {
|
||||
// FIXME - add some check here ROFL
|
||||
console.log("INPUT: ", data)
|
||||
@@ -244,8 +307,8 @@ const Admin = (props) => {
|
||||
}
|
||||
|
||||
const paperStyle = {
|
||||
minWidth: "100%",
|
||||
maxWidth: "100%",
|
||||
maxWidth: 1250,
|
||||
margin: "auto",
|
||||
color: "white",
|
||||
backgroundColor: surfaceColor,
|
||||
marginBottom: 10,
|
||||
@@ -256,6 +319,101 @@ const Admin = (props) => {
|
||||
modalUser[field] = value
|
||||
}
|
||||
|
||||
const generateApikey = () => {
|
||||
fetch(globalUrl+"/api/v1/generateapikey", {
|
||||
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!")
|
||||
} else {
|
||||
getUsers()
|
||||
}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
console.log("RESP: ", responseJson)
|
||||
if (!responseJson.success && responseJson.reason !== undefined) {
|
||||
alert.error("Failed getting new: "+responseJson.reason)
|
||||
} else {
|
||||
alert.success("Got new API key")
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const editUserModal =
|
||||
<Dialog modal
|
||||
open={selectedUserModalOpen}
|
||||
onClose={() => {setSelectedUserModalOpen(false)}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
color: "white",
|
||||
minWidth: "800px",
|
||||
minHeight: "320px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle><span style={{color: "white"}}>Edit user</span></DialogTitle>
|
||||
<DialogContent>
|
||||
<div style={{display: "flex"}}>
|
||||
<TextField
|
||||
style={{backgroundColor: inputColor, flex: 3}}
|
||||
InputProps={{
|
||||
style:{
|
||||
height: 50,
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
required
|
||||
fullWidth={true}
|
||||
placeholder="New password"
|
||||
type="password"
|
||||
id="standard-required"
|
||||
autoComplete="password"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
style={{maxHeight: 50, flex: 1}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => onPasswordChange()}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, backgroundColor: inputColor}}/>
|
||||
<Button
|
||||
style={{}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => deleteUser(selectedUser)}
|
||||
>
|
||||
{selectedUser.active ? "Deactivate" : "Activate"}
|
||||
</Button>
|
||||
<Button
|
||||
style={{}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => generateApikey(selectedUser)}
|
||||
>
|
||||
Get new API key
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
const modalView =
|
||||
<Dialog modal
|
||||
open={modalOpen}
|
||||
@@ -373,11 +531,68 @@ const Admin = (props) => {
|
||||
</Button>
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, backgroundColor: inputColor}}/>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Username"
|
||||
style={{minWidth: 200, maxWidth: 200}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="API key"
|
||||
style={{minWidth: 350, maxWidth: 350, overflow: "hidden"}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Password"
|
||||
style={{minWidth: 180, maxWidth: 180}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Role"
|
||||
style={{minWidth: 150, maxWidth: 150}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Active"
|
||||
style={{minWidth: 180, maxWidth: 180}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Actions"
|
||||
style={{minWidth: 180, maxWidth: 180}}
|
||||
/>
|
||||
</ListItem>
|
||||
{users === undefined ? null : users.map(data => {
|
||||
console.log(data)
|
||||
return (
|
||||
<ListItem>
|
||||
{data.Username}
|
||||
<ListItemText
|
||||
primary={data.username}
|
||||
style={{minWidth: 200, maxWidth: 200}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.apikey === undefined || data.apikey.length === 0 ? "" : data.apikey}
|
||||
style={{maxWidth: 350, minWidth: 350,}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="**************"
|
||||
style={{minWidth: 180, maxWidth: 180}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.role}
|
||||
style={{minWidth: 150, maxWidth: 150}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.active ? "True" : "False"}
|
||||
style={{minWidth: 180, maxWidth: 180}}
|
||||
/>
|
||||
<ListItemText style={{display: "flex"}}>
|
||||
<Button
|
||||
style={{}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSelectedUserModalOpen(true)
|
||||
setSelectedUser(data)
|
||||
}}
|
||||
>
|
||||
Edit user
|
||||
</Button>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
@@ -472,7 +687,7 @@ const Admin = (props) => {
|
||||
}
|
||||
|
||||
const data =
|
||||
<div style={{width: 1366, margin: "auto"}}>
|
||||
<div style={{minWidth: 1366, margin: "auto"}}>
|
||||
<Paper style={paperStyle}>
|
||||
<Tabs
|
||||
value={curTab}
|
||||
@@ -495,6 +710,7 @@ const Admin = (props) => {
|
||||
return (
|
||||
<div>
|
||||
{modalView}
|
||||
{editUserModal}
|
||||
{data}
|
||||
</div>
|
||||
)
|
||||
|
||||
+329
-89
@@ -20,6 +20,7 @@ import Divider from '@material-ui/core/Divider';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
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';
|
||||
@@ -112,6 +113,7 @@ const AngularWorkflow = (props) => {
|
||||
|
||||
const [appAuthentication, setAppAuthentication] = React.useState({});
|
||||
const [variablesModalOpen, setVariablesModalOpen] = React.useState(false);
|
||||
const [executionVariablesModalOpen, setExecutionVariablesModalOpen] = React.useState(false);
|
||||
const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false);
|
||||
const [conditionsModalOpen, setConditionsModalOpen] = React.useState(false);
|
||||
const [newVariableName, setNewVariableName] = React.useState("");
|
||||
@@ -323,6 +325,14 @@ const AngularWorkflow = (props) => {
|
||||
incomingEdges.addClass('success-highlight')
|
||||
currentnode.addClass('executing-highlight')
|
||||
break
|
||||
case "SKIPPED":
|
||||
currentnode.removeClass('not-executing-highlight')
|
||||
currentnode.removeClass('success-highlight')
|
||||
currentnode.removeClass('failure-highlight')
|
||||
currentnode.removeClass('awaiting-data-highlight')
|
||||
currentnode.removeClass('executing-highlight')
|
||||
currentnode.addClass('skipped-highlight')
|
||||
break
|
||||
case "WAITING":
|
||||
currentnode.removeClass('not-executing-highlight')
|
||||
currentnode.removeClass('success-highlight')
|
||||
@@ -785,15 +795,14 @@ const AngularWorkflow = (props) => {
|
||||
setSelectedAction({})
|
||||
setSelectedTrigger({})
|
||||
} else {
|
||||
alert.info("Can't edit branches from triggers")
|
||||
//alert.info("Can't edit branches from triggers")
|
||||
}
|
||||
}
|
||||
|
||||
const onNodeSelect = (event) => {
|
||||
const data = event.target.data()
|
||||
console.log("NODE: ", data)
|
||||
setLastSaved(false)
|
||||
//console.log(data)
|
||||
console.log(data)
|
||||
|
||||
if (data.type === "ACTION") {
|
||||
// FIXME - unselect
|
||||
@@ -905,6 +914,21 @@ const AngularWorkflow = (props) => {
|
||||
if (node.isNode() && cy.nodes().size() === 1) {
|
||||
//setStartNode(node.data('id'))
|
||||
workflow.start = node.data('id')
|
||||
setWorkflow(workflow)
|
||||
} else {
|
||||
if (workflow.actions === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove bad startnode
|
||||
const startnode_exists = false
|
||||
for (var key in workflow.actions) {
|
||||
const action = workflow.actions[key]
|
||||
if (action.isStartNode && workflow.start !== action.id) {
|
||||
action.isStartNode = false
|
||||
}
|
||||
}
|
||||
|
||||
setWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
@@ -944,7 +968,7 @@ const AngularWorkflow = (props) => {
|
||||
alert.success("Changed startnode to "+ele.data()["label"])
|
||||
ele.data("isStartNode", true)
|
||||
workflow.start = ele.id()
|
||||
return
|
||||
return true
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1412,22 +1436,6 @@ const AngularWorkflow = (props) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
|
||||
if (workflow.workflow_variables === undefined || workflow.workflow_variables === null || workflow.workflow_variables.length === 0) {
|
||||
return (
|
||||
<div style={appViewStyle}>
|
||||
<div style={appScrollStyle}>
|
||||
<div style={{margin: 10}}>
|
||||
Looks like you don't have any variables yet.
|
||||
<div/>
|
||||
<div style={{width: "100%", margin: "auto"}}>
|
||||
<Button fullWidth style={{margin: "auto", marginTop: "10px", borderRadius: 0}} color="primary" variant="outlined" onClick={() => setVariablesModalOpen(true)}>Make a new workflow variable</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const menuClick = (event) => {
|
||||
setOpen(!open)
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -1438,8 +1446,13 @@ const AngularWorkflow = (props) => {
|
||||
setWorkflow(workflow)
|
||||
}
|
||||
|
||||
const deleteExecutionVariable = (variableName) => {
|
||||
workflow.execution_variables = workflow.execution_variables.filter(data => data.name !== variableName)
|
||||
setWorkflow(workflow)
|
||||
}
|
||||
|
||||
const variableScrollStyle = {
|
||||
marginTop: "10px",
|
||||
margin: 15,
|
||||
overflow: "scroll",
|
||||
height: "66vh",
|
||||
overflowX: "auto",
|
||||
@@ -1450,7 +1463,9 @@ const AngularWorkflow = (props) => {
|
||||
return (
|
||||
<div style={appViewStyle}>
|
||||
<div style={variableScrollStyle}>
|
||||
{workflow.workflow_variables.map(variable=> {
|
||||
What are <a href="https://shuffler.io/docs/workflows#workflow_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>WORKFLOW variables?</a>
|
||||
{workflow.workflow_variables === null ?
|
||||
null : workflow.workflow_variables.map(variable=> {
|
||||
return (
|
||||
<div>
|
||||
<Paper square style={paperVariableStyle} onClick={() => {
|
||||
@@ -1508,10 +1523,69 @@ const AngularWorkflow = (props) => {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div style={{flex: "1"}}>
|
||||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => setVariablesModalOpen(true)}>New workflow variable</Button>
|
||||
</div>
|
||||
<Divider style={{marginBottom: 20, marginTop: 20, height: 1, width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||||
What are <a href="https://shuffler.io/docs/workflows#execution_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>EXECUTION variables?</a>
|
||||
{workflow.execution_variables === null || workflow.execution_variables === undefined ?
|
||||
null : workflow.execution_variables.map(variable=> {
|
||||
return (
|
||||
<div>
|
||||
<Paper square style={paperVariableStyle} onClick={() => {
|
||||
}}>
|
||||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: "orange", marginRight: "5px"}} />
|
||||
<div style={{display: "flex", width: "100%"}}>
|
||||
<div style={{flex: "10", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}} onClick={() => {
|
||||
setNewVariableName(variable.name)
|
||||
setExecutionVariablesModalOpen(true)}}>
|
||||
Name: {variable.name}
|
||||
</div>
|
||||
<div style={{flex: "1", marginLeft: "0px"}}>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={menuClick}
|
||||
style={{color: "white"}}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="long-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={open}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
setAnchorEl(null)
|
||||
}}
|
||||
>
|
||||
|
||||
</div>
|
||||
<div style={{flex: "1"}}>
|
||||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => setVariablesModalOpen(true)}>New workflow variable</Button>
|
||||
<MenuItem style={{backgroundColor: surfaceColor, color: "white"}} onClick={() => {
|
||||
setOpen(false)
|
||||
setNewVariableName(variable.name)
|
||||
setExecutionVariablesModalOpen(true)
|
||||
}} key={"Edit"}>{"Edit"}</MenuItem>
|
||||
<MenuItem style={{backgroundColor: surfaceColor, color: "white"}} onClick={() => {
|
||||
deleteExecutionVariable(variable.name)
|
||||
setOpen(false)
|
||||
}} key={"Delete"}>{"Delete"}</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div style={{flex: "1"}}>
|
||||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => setExecutionVariablesModalOpen(true)}>New execution variable</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1862,6 +1936,7 @@ const AngularWorkflow = (props) => {
|
||||
isStartNode: false,
|
||||
large_image: app.large_image,
|
||||
authentication: [],
|
||||
execution_variable: undefined,
|
||||
}
|
||||
|
||||
// const image = "url("+app.large_image+")"
|
||||
@@ -2075,7 +2150,14 @@ const AngularWorkflow = (props) => {
|
||||
// appname & version
|
||||
// description
|
||||
// ACTION select
|
||||
//
|
||||
const selectedNameChange = (event) => {
|
||||
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(",", "")
|
||||
selectedAction.label = event.target.value
|
||||
setSelectedAction(selectedAction)
|
||||
}
|
||||
@@ -2361,7 +2443,7 @@ const AngularWorkflow = (props) => {
|
||||
|
||||
} else if (data.variant === "WORKFLOW_VARIABLE") {
|
||||
varcolor = "#f85a3e"
|
||||
if (workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) {
|
||||
if ((workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) && (workflow.execution_variables === null || workflow.execution_variables === undefined || workflow.execution_variables.length === 0)) {
|
||||
setCurrentView(2)
|
||||
datafield =
|
||||
<div>
|
||||
@@ -2389,15 +2471,22 @@ const AngularWorkflow = (props) => {
|
||||
fullWidth
|
||||
value={selectedAction.parameters[count].action_field}
|
||||
onChange={(e) => {
|
||||
console.log(e.target.value)
|
||||
changeActionParameterVariable(e.target.value, count)
|
||||
}}
|
||||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||||
>
|
||||
{workflow.workflow_variables.map(data => (
|
||||
{workflow.workflow_variables !== null ? workflow.workflow_variables.map(data => (
|
||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
|
||||
{data.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
)) : null}
|
||||
<Divider />
|
||||
{workflow.execution_variables !== null ? workflow.execution_variables.map(data => (
|
||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
|
||||
{data.name}
|
||||
</MenuItem>
|
||||
)) : null}
|
||||
</Select>
|
||||
}
|
||||
|
||||
@@ -2466,33 +2555,40 @@ const AngularWorkflow = (props) => {
|
||||
if (oldstartnode.length > 0) {
|
||||
oldstartnode[0].data("isStartNode", false)
|
||||
var oldnodecnt = workflow.actions.findIndex(a => a.id === workflow.start)
|
||||
workflow.actions[oldnodecnt].isStartNode = false
|
||||
if (workflow.actions[oldnodecnt] !== undefined) {
|
||||
workflow.actions[oldnodecnt].isStartNode = false
|
||||
}
|
||||
}
|
||||
|
||||
var newstartnode = cy.getElementById(selectedAction.id)
|
||||
if (newstartnode.length > 0) {
|
||||
newstartnode[0].data("isStartNode", true)
|
||||
var newnodecnt = workflow.actions.findIndex(a => a.id === selectedAction.id)
|
||||
workflow.actions[newnodecnt].isStartNode = true
|
||||
console.log("NEW NODE CNT: ", newnodecnt)
|
||||
if (workflow.actions[newnodecnt] !== undefined) {
|
||||
workflow.actions[newnodecnt].isStartNode = true
|
||||
console.log(workflow.actions[newnodecnt])
|
||||
}
|
||||
}
|
||||
|
||||
// Find branches with triggers as source nodes
|
||||
// Move these targets to be the new node
|
||||
// Set arrows pointing to new startnode with errors
|
||||
for (var key in workflow.branches) {
|
||||
var item = workflow.branches[key]
|
||||
if (item.destination_id === oldstartnode[0].data()["id"]) {
|
||||
var curbranch = cy.getElementById(item.id)
|
||||
if (curbranch.length > 0) {
|
||||
//console.log(curbranch[0].data())
|
||||
//curbranch[0].data("target", selectedAction.id)
|
||||
curbranch[0].data("hasErrors", true)
|
||||
//workflow.branches[key].destination_id = selectedAction.id
|
||||
//console.log(curbranch[0].data())
|
||||
}
|
||||
}
|
||||
}
|
||||
//for (var key in workflow.branches) {
|
||||
// var item = workflow.branches[key]
|
||||
// if (item.destination_id === oldstartnode[0].data()["id"]) {
|
||||
// var curbranch = cy.getElementById(item.id)
|
||||
// if (curbranch.length > 0) {
|
||||
// //console.log(curbranch[0].data())
|
||||
// //curbranch[0].data("target", selectedAction.id)
|
||||
// //curbranch[0].data("hasErrors", true)
|
||||
// //workflow.branches[key].destination_id = selectedAction.id
|
||||
// //console.log(curbranch[0].data())
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
setUpdate("start_node"+selectedAction.id)
|
||||
workflow.start = selectedAction.id
|
||||
setWorkflow(workflow)
|
||||
//setStartNode(selectedAction.id)
|
||||
@@ -2510,7 +2606,7 @@ const AngularWorkflow = (props) => {
|
||||
<div style={{display: "flex", minHeight: 40, marginBottom: 30}}>
|
||||
<div style={{flex: 1}}>
|
||||
<h3 style={{marginBottom: 5}}>{selectedAction.app_name}</h3>
|
||||
<Link to="/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}}>What are apps?</Link>
|
||||
<a href="https://shuffler.io/docs/apps#actions" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>What are actions?</a>
|
||||
{selectedAction.errors !== null && selectedAction.errors.length > 0 ?
|
||||
<div>
|
||||
Errors: {selectedAction.errors.join("\n")}
|
||||
@@ -2544,39 +2640,79 @@ const AngularWorkflow = (props) => {
|
||||
placeholder={selectedAction.label}
|
||||
onChange={selectedNameChange}
|
||||
/>
|
||||
|
||||
<div style={{marginTop: "20px"}}>
|
||||
Environment
|
||||
<Select
|
||||
value={selectedActionEnvironment === undefined || selectedActionEnvironment.Name === undefined ? "" : selectedActionEnvironment.Name}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: inputColor,
|
||||
}
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
const env = environments.find(a => a.Name === e.target.value)
|
||||
console.log("FOUND: ", e.target.value)
|
||||
console.log("FOUND2: ", env)
|
||||
setSelectedActionEnvironment(env)
|
||||
selectedAction.environment = env.Name
|
||||
setSelectedAction(selectedAction)
|
||||
}}
|
||||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||||
>
|
||||
{environments.map(data => (
|
||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.Name}>
|
||||
{data.Name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{environments !== undefined && environments !== null && environments.length > 1 ?
|
||||
<div style={{marginTop: "20px"}}>
|
||||
Environment
|
||||
<Select
|
||||
value={selectedActionEnvironment === undefined || selectedActionEnvironment.Name === undefined ? "" : selectedActionEnvironment.Name}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: inputColor,
|
||||
}
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
const env = environments.find(a => a.Name === e.target.value)
|
||||
setSelectedActionEnvironment(env)
|
||||
selectedAction.environment = env.Name
|
||||
setSelectedAction(selectedAction)
|
||||
}}
|
||||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||||
>
|
||||
{environments.map(data => (
|
||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.Name}>
|
||||
{data.Name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
: null}
|
||||
{workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > 0 ?
|
||||
<div style={{marginTop: "20px"}}>
|
||||
Set execution variable (optional)
|
||||
<Select
|
||||
value={selectedAction.execution_variable !== undefined ? selectedAction.execution_variable.name : "No selection"}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: inputColor,
|
||||
}
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "No selection") {
|
||||
selectedAction.execution_variable = {"name": "No selection"}
|
||||
} else {
|
||||
const value = workflow.execution_variables.find(a => a.name === e.target.value)
|
||||
console.log("FOUND: ", value)
|
||||
selectedAction.execution_variable = value
|
||||
}
|
||||
setSelectedAction(selectedAction)
|
||||
setUpdate("actionname "+e.target.value+selectedAction.Label)
|
||||
}}
|
||||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||||
>
|
||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value="No selection">
|
||||
<em>No selection</em>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
{workflow.execution_variables.map(data => (
|
||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
|
||||
{data.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
: null}
|
||||
{/*requiresAuthentication ?
|
||||
<div style={{marginTop: "20px"}}>
|
||||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="contained" onClick={() => setAuthenticationModalOpen(true)}>
|
||||
@@ -2771,12 +2907,12 @@ const AngularWorkflow = (props) => {
|
||||
|
||||
const AppConditionHandler = (props) => {
|
||||
const { tmpdata, type } = props
|
||||
const [data, ] = useState(tmpdata)
|
||||
const [multiline, setMultiline] = useState(false)
|
||||
|
||||
if (tmpdata === undefined) {
|
||||
return tmpdata
|
||||
}
|
||||
const [data, ] = useState(tmpdata)
|
||||
const [multiline, setMultiline] = useState(false)
|
||||
|
||||
if (data.variant === "") {
|
||||
data.variant = "STATIC_VALUE"
|
||||
@@ -3246,7 +3382,7 @@ const AngularWorkflow = (props) => {
|
||||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||||
<div style={{flex: "1"}}>
|
||||
<h3 style={{marginBottom: "5px"}} >Branch: Conditions - {selectedEdgeIndex}</h3>
|
||||
<Link to="/docs/conditions" style={{textDecoration: "none", color: "#f85a3e"}}>What are conditions?</Link>
|
||||
<a href="https://shuffler.io/docs/conditions" style={{textDecoration: "none", color: "#f85a3e"}}>What are conditions?</a>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||||
@@ -3476,7 +3612,7 @@ const AngularWorkflow = (props) => {
|
||||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||||
<div style={{flex: "1"}}>
|
||||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||||
<Link to="/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</Link>
|
||||
<a href="https://shuffler.io/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</a>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||||
@@ -3562,7 +3698,7 @@ const AngularWorkflow = (props) => {
|
||||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||||
<div style={{flex: "1"}}>
|
||||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||||
<Link to="/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</Link>
|
||||
<a href="https://shuffler.io/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</a>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||||
@@ -3912,7 +4048,7 @@ const AngularWorkflow = (props) => {
|
||||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||||
<div style={{flex: "1"}}>
|
||||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||||
<Link to="/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</Link>
|
||||
<a href="https://shuffler.io/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</a>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||||
@@ -4041,7 +4177,7 @@ const AngularWorkflow = (props) => {
|
||||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||||
<div style={{flex: "1"}}>
|
||||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||||
<Link to="/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</Link>
|
||||
<a href="https://shuffler.io/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</a>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||||
@@ -4310,7 +4446,7 @@ const AngularWorkflow = (props) => {
|
||||
)
|
||||
} else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
|
||||
if (selectedTrigger.trigger_type === "SCHEDULE") {
|
||||
console.log("SCHEDULE")
|
||||
//console.log("SCHEDULE")
|
||||
return(
|
||||
<div style={rightsidebarStyle}>
|
||||
<ScheduleSidebar />
|
||||
@@ -4493,10 +4629,15 @@ const AngularWorkflow = (props) => {
|
||||
executionData.results.map(data => {
|
||||
var showResult = data.result.trim()
|
||||
showResult.split(" None").join(" \"None\"")
|
||||
//showResult = replaceAll(showResult, " None", " \"None\"")
|
||||
|
||||
// showResult = replaceAll(showResult, " None", " \"None\"")
|
||||
// Super basic check.
|
||||
var jsonvalid = true
|
||||
try {
|
||||
JSON.parse(showResult)
|
||||
const tmp = String(JSON.parse(showResult))
|
||||
if (!tmp.includes("{") && !tmp.includes("[")) {
|
||||
jsonvalid = false
|
||||
}
|
||||
} catch (e) {
|
||||
jsonvalid = false
|
||||
}
|
||||
@@ -4550,7 +4691,7 @@ const AngularWorkflow = (props) => {
|
||||
cy={(incy) => {
|
||||
// FIXME: There's something specific loading when
|
||||
// you do the first hover of a node. Why is this different?
|
||||
console.log("CY: ", incy)
|
||||
//console.log("CY: ", incy)
|
||||
setCy(incy)
|
||||
}}
|
||||
/>
|
||||
@@ -4564,6 +4705,104 @@ const AngularWorkflow = (props) => {
|
||||
<div style={{color: "white"}}>
|
||||
TMP FOR NOT LOGGED IN
|
||||
</div>
|
||||
|
||||
const executionVariableModal = executionVariablesModalOpen ?
|
||||
<Dialog modal
|
||||
open={executionVariablesModalOpen}
|
||||
onClose={() => {
|
||||
setNewVariableName("")
|
||||
setExecutionVariablesModalOpen(false)
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<DialogTitle><span style={{color: "white"}}>Execution Variable</span></DialogTitle>
|
||||
<DialogContent>
|
||||
Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more <a href="https://shuffler.io/docs/workflow#execution_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>here</a>
|
||||
<TextField
|
||||
onBlur={(event) => setNewVariableName(event.target.value)}
|
||||
color="primary"
|
||||
placeholder="Name"
|
||||
style={{marginTop: 25}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white"
|
||||
}
|
||||
}}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
defaultValue={newVariableName}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
style={{borderRadius: "0px"}}
|
||||
onClick={() => {
|
||||
setNewVariableName("")
|
||||
setExecutionVariablesModalOpen(false)
|
||||
}} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={{borderRadius: "0px"}} disabled={newVariableName.length === 0} onClick={() => {
|
||||
console.log("VARIABLES! ", newVariableName)
|
||||
if (workflow.execution_variables === undefined || workflow.execution_variables === null) {
|
||||
workflow.execution_variables = []
|
||||
}
|
||||
|
||||
// try to find one with the same name
|
||||
const found = workflow.execution_variables.findIndex(data => data.name === newVariableName)
|
||||
//console.log(found)
|
||||
if (found !== -1) {
|
||||
if (newVariableName.length > 0) {
|
||||
workflow.execution_variables[found].name = newVariableName
|
||||
}
|
||||
} else {
|
||||
workflow.execution_variables.push({
|
||||
"name": newVariableName,
|
||||
"description": "An execution variable",
|
||||
"value": "",
|
||||
"id": uuid.v4(),
|
||||
})
|
||||
}
|
||||
|
||||
setExecutionVariablesModalOpen(false)
|
||||
setNewVariableName("")
|
||||
setWorkflow(workflow)
|
||||
}} color="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
{workflowExecutions.length > 0 ?
|
||||
<DialogContent>
|
||||
<Divider style={{backgroundColor: "white", marginTop: 15, marginBottom: 15,}}/>
|
||||
<b style={{marginBottom: 10}}>Values from last 3 executions</b>
|
||||
{workflowExecutions.slice(0,3).map((execution, index) => {
|
||||
if (execution.execution_variables === undefined || execution.execution_variables === null || execution.execution_variables === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const variable = execution.execution_variables.find(data => data.name === newVariableName)
|
||||
if (variable === undefined || variable.value === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{index+1}: {variable.value}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DialogContent>
|
||||
: null
|
||||
}
|
||||
</FormControl>
|
||||
</Dialog>
|
||||
: null
|
||||
|
||||
const variablesModal = variablesModalOpen ?
|
||||
<Dialog modal
|
||||
@@ -4779,7 +5018,7 @@ const AngularWorkflow = (props) => {
|
||||
>
|
||||
<DialogTitle><div style={{color: "white"}}>Authentication for {selectedApp.name}</div></DialogTitle>
|
||||
<DialogContent>
|
||||
<Link to="/docs/apps#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</Link>
|
||||
<a href="https://shuffler.io/docs/apps#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</a>
|
||||
<div />
|
||||
{selectedApp.link.length > 0 ? <EndpointData /> : null}
|
||||
<div style={{marginTop: 15, marginBottom: 15, }}/>
|
||||
@@ -4803,6 +5042,7 @@ const AngularWorkflow = (props) => {
|
||||
<div>
|
||||
{newView}
|
||||
{variablesModal}
|
||||
{executionVariableModal}
|
||||
{conditionsModal}
|
||||
{authenticationModal}
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -39,8 +39,10 @@ import { positions, Provider } from "react-alert";
|
||||
|
||||
// Production - backend proxy forwarding in nginx
|
||||
var globalUrl = window.location.origin
|
||||
|
||||
// CORS used for testing purposes. Should only happen with specific port and http
|
||||
if (window.location.protocol == "http:" && window.location.port === "3000") {
|
||||
globalUrl = "http://192.168.3.6:5001"
|
||||
globalUrl = "http://localhost:5001"
|
||||
}
|
||||
|
||||
const surfaceColor = "#27292D"
|
||||
@@ -90,13 +92,11 @@ const App = (message, props) => {
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(responseJson => {
|
||||
console.log(responseJson)
|
||||
if (responseJson.success === true) {
|
||||
setUserData(responseJson)
|
||||
setIsLoggedIn(true)
|
||||
|
||||
// Updating cookie every request
|
||||
console.log("COOKIES: ", cookies)
|
||||
for (var key in responseJson["cookies"]) {
|
||||
setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, {path: "/"})
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ const AppCreator = (props) => {
|
||||
const checkQuery = () => {
|
||||
var urlParams = new URLSearchParams(window.location.search)
|
||||
if (!urlParams.has("id")) {
|
||||
setIsAppLoaded(true)
|
||||
setIsAppLoaded(true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -362,7 +362,10 @@ const AppCreator = (props) => {
|
||||
}
|
||||
|
||||
if (data.servers !== undefined && data.servers.length > 0) {
|
||||
setBaseUrl(data.servers[0].url)
|
||||
var firstUrl = data.servers[0].url
|
||||
if (firstUrl.endsWith("/")) {
|
||||
setBaseUrl(firstUrl.slice(0, firstUrl.length-1))
|
||||
}
|
||||
}
|
||||
|
||||
// This is annoying (:
|
||||
@@ -380,6 +383,7 @@ const AppCreator = (props) => {
|
||||
|
||||
// FIXME - headers?
|
||||
var newActions = []
|
||||
var wordlist = {}
|
||||
for (let [path, pathvalue] of Object.entries(data.paths)) {
|
||||
for (let [method, methodvalue] of Object.entries(pathvalue)) {
|
||||
var newaction = {
|
||||
@@ -426,6 +430,61 @@ 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
|
||||
// Should find others with the same START to their path
|
||||
// Make a list of reserved names? Aka things that show up only once
|
||||
if (Object.getOwnPropertyNames(wordlist).length === 0) {
|
||||
for (let [newpath, pathvalue] of Object.entries(data.paths)) {
|
||||
const newpathsplit = newpath.split("/")
|
||||
for(var key in newpathsplit) {
|
||||
const pathitem = newpathsplit[key].toLowerCase()
|
||||
if (wordlist[pathitem] === undefined) {
|
||||
wordlist[pathitem] = 1
|
||||
} else {
|
||||
wordlist[pathitem] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("WORDLIST: ", wordlist)
|
||||
|
||||
// Remove underscores and make it normal with upper case etc
|
||||
const urlsplit = path.split("/")
|
||||
if (urlsplit.length > 0) {
|
||||
var curname = ""
|
||||
for(var key in urlsplit) {
|
||||
var subpath = urlsplit[key]
|
||||
if (wordlist[subpath] > 2 || subpath.length < 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
curname = subpath
|
||||
break
|
||||
}
|
||||
|
||||
// FIXME: If name exists,
|
||||
// FIXME: Check if first part of parsedname is verb, otherwise use method
|
||||
const parsedname = curname.split("_").join(" ").split("-").join(" ").split("{").join(" ").split("}").join(" ").trim()
|
||||
if (parsedname.length === 0) {
|
||||
newaction.errors.push("Missing name")
|
||||
} else {
|
||||
const newname = method.charAt(0).toUpperCase() + method.slice(1) + " " + parsedname
|
||||
const searchactions = newActions.find(data => data.name === newname)
|
||||
console.log("SEARCH: ", searchactions)
|
||||
if (searchactions !== undefined) {
|
||||
newaction.errors.push("Missing name")
|
||||
} else {
|
||||
newaction.name = newname
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
newaction.errors.push("Missing name")
|
||||
}
|
||||
}
|
||||
newActions.push(newaction)
|
||||
}
|
||||
}
|
||||
@@ -580,7 +639,7 @@ const AppCreator = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (item.body.length > 0) {
|
||||
if (item.body !== undefined && item.body.length > 0) {
|
||||
const required = false
|
||||
newitem = {
|
||||
"in": "body",
|
||||
@@ -1013,7 +1072,7 @@ const AppCreator = (props) => {
|
||||
|
||||
const getActionErrors = () => {
|
||||
var errormessage = []
|
||||
if (currentAction.name.length === 0) {
|
||||
if (currentAction.name === undefined || currentAction.name.length === 0) {
|
||||
errormessage.push("Name can't be empty")
|
||||
}
|
||||
|
||||
@@ -1466,7 +1525,7 @@ const AppCreator = (props) => {
|
||||
// <img src={file} id="logo" style={{width: "100%", height: "100%"}} />
|
||||
|
||||
const imageData = file.length > 0 ? file : fileBase64
|
||||
const imageInfo = <img src={imageData} alt="Click to upload an image" id="logo" style={{maxWidth: 174, maxHeight: 174,}} />
|
||||
const imageInfo = <img src={imageData} alt="Click to upload an image (174x174)" id="logo" style={{maxWidth: 174, maxHeight: 174,}} />
|
||||
|
||||
// Random names for type & autoComplete. Didn't research :^)
|
||||
const landingpageDataBrowser =
|
||||
|
||||
@@ -457,7 +457,7 @@ const Apps = (props) => {
|
||||
<Paper square style={uploadViewPaperStyle}>
|
||||
<div style={{width: "100%", margin: 25}}>
|
||||
<h2>App Creator</h2>
|
||||
<a href="/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">How it works</a>
|
||||
<a href="https://shuffler.io/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">How it works</a>
|
||||
- <a href="https://github.com/frikky/security-openapis" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">Security API's</a>
|
||||
- <a href="https://apis.guru/browse-apis/" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">OpenAPI directory</a>
|
||||
<div/>
|
||||
@@ -500,6 +500,10 @@ const Apps = (props) => {
|
||||
}
|
||||
|
||||
const handleSearchChange = (search) => {
|
||||
if (apps === undefined || apps === null || apps.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const searchfield = search.toLowerCase()
|
||||
const newapps = apps.filter(data => data.name.toLowerCase().includes(searchfield) || data.description.toLowerCase().includes(searchfield))
|
||||
|
||||
@@ -901,6 +905,7 @@ const Apps = (props) => {
|
||||
</Dialog>
|
||||
: null
|
||||
|
||||
const circularLoader = validation ? <CircularProgress color="primary" /> : null
|
||||
const appsModalLoad = loadAppsModalOpen ?
|
||||
<Dialog modal
|
||||
open={loadAppsModalOpen}
|
||||
@@ -994,7 +999,6 @@ const Apps = (props) => {
|
||||
: null
|
||||
|
||||
const errorText = openApiError.length > 0 ? <div>Error: {openApiError}</div> : null
|
||||
const circularLoader = validation ? <CircularProgress color="primary" /> : null
|
||||
const modalView = openApiModal ?
|
||||
<Dialog modal
|
||||
open={openApiModal}
|
||||
|
||||
@@ -189,10 +189,10 @@ const Docs = (props) => {
|
||||
}
|
||||
|
||||
function Heading(props) {
|
||||
const element = React.createElement(`h${props.level}`, {style: {marginTop: 25}}, props.children)
|
||||
const element = React.createElement(`h${props.level}`, {style: {marginTop: 40}}, props.children)
|
||||
return (
|
||||
<span>
|
||||
{props.level !== 1 ? <Divider style={{width: "90%", marginTop: 25, backgroundColor: inputColor}} /> : null}
|
||||
{props.level !== 1 ? <Divider style={{width: "90%", marginTop: 40, backgroundColor: inputColor}} /> : null}
|
||||
{element}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import Divider from '@material-ui/core/Divider';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import { useAlert } from "react-alert";
|
||||
|
||||
|
||||
//const tmpdata = {
|
||||
@@ -23,6 +24,7 @@ import TextField from '@material-ui/core/TextField';
|
||||
// FIXME: Use isLoggedIn :)
|
||||
const Settings = (props) => {
|
||||
const { globalUrl, isLoaded, userdata, surfaceColor, inputColor } = props;
|
||||
const alert = useAlert()
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [firstname, setFirstname] = useState("");
|
||||
@@ -65,7 +67,7 @@ const Settings = (props) => {
|
||||
}
|
||||
|
||||
const onPasswordChange = () => {
|
||||
const data = {"currentpassword": currentPassword, "newpassword": newPassword, "newpassword2": newPassword2}
|
||||
const data = {"username": userSettings.username, "currentpassword": currentPassword, "newpassword": newPassword, "newpassword2": newPassword2}
|
||||
const url = globalUrl+'/api/v1/passwordchange';
|
||||
fetch(url, {
|
||||
mode: 'cors',
|
||||
@@ -82,7 +84,10 @@ const Settings = (props) => {
|
||||
response.json().then(responseJson => {
|
||||
if (responseJson["success"] === false) {
|
||||
setPasswordFormMessage(responseJson["reason"])
|
||||
}
|
||||
} else {
|
||||
alert.success("Changed password!")
|
||||
setPasswordFormMessage("")
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch(error => {
|
||||
@@ -183,7 +188,7 @@ const Settings = (props) => {
|
||||
<div style={{display: "flex", marginTop: "80px"}}>
|
||||
<Paper style={boxStyle}>
|
||||
<h2>APIKEY</h2>
|
||||
<Link to="/docs/api#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is the API key used for?</Link>
|
||||
<Link to="/docs/API#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is the API key used for?</Link>
|
||||
<TextField
|
||||
style={{backgroundColor: inputColor, flex: "1"}}
|
||||
InputProps={{
|
||||
@@ -209,6 +214,7 @@ const Settings = (props) => {
|
||||
onClick={() => generateApikey()}
|
||||
>Re-Generate APIKEY</Button>
|
||||
<Divider style={{marginTop: "40px"}}/>
|
||||
{/*
|
||||
<h2>Settings</h2>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
|
||||
<TextField
|
||||
@@ -369,6 +375,7 @@ const Settings = (props) => {
|
||||
</Button>
|
||||
<h3>{formMessage}</h3>
|
||||
<Divider />
|
||||
*/}
|
||||
<h2>Password</h2>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
|
||||
<TextField
|
||||
|
||||
+29
-10
@@ -518,7 +518,10 @@ const Workflows = (props) => {
|
||||
var showResult = data.result.trim()
|
||||
showResult = replaceAll(showResult, " None", " \"None\"");
|
||||
try {
|
||||
JSON.parse(showResult)
|
||||
const tmp = String(JSON.parse(showResult))
|
||||
if (!tmp.includes("{") && !tmp.includes("[")) {
|
||||
jsonvalid = false
|
||||
}
|
||||
} catch (e) {
|
||||
jsonvalid = false
|
||||
}
|
||||
@@ -611,7 +614,10 @@ const Workflows = (props) => {
|
||||
showResult = replaceAll(showResult, " None", " \"None\"");
|
||||
|
||||
try {
|
||||
JSON.parse(showResult)
|
||||
const tmp = String(JSON.parse(showResult))
|
||||
if (!tmp.includes("{") && !tmp.includes("[")) {
|
||||
jsonvalid = false
|
||||
}
|
||||
} catch (e) {
|
||||
jsonvalid = false
|
||||
}
|
||||
@@ -634,7 +640,10 @@ const Workflows = (props) => {
|
||||
showResult = replaceAll(showResult, " None", " \"None\"");
|
||||
|
||||
try {
|
||||
JSON.parse(showResult)
|
||||
const tmp = JSON.parse(showResult)
|
||||
if (!tmp.includes("{") && !tmp.includes("[")) {
|
||||
jsonvalid = false
|
||||
}
|
||||
} catch (e) {
|
||||
jsonvalid = false
|
||||
}
|
||||
@@ -654,6 +663,9 @@ const Workflows = (props) => {
|
||||
<div>
|
||||
ID: {selectedExecution.execution_id}
|
||||
</div>
|
||||
<div>
|
||||
<b>Last node:</b> {selectedExecution.workflow.actions.find(data => data.id === selectedExecution.last_node).actions[0].label}
|
||||
</div>
|
||||
*/
|
||||
if (Object.getOwnPropertyNames(selectedExecution).length > 0 && selectedExecution.workflow.actions !== null) {
|
||||
return (
|
||||
@@ -667,12 +679,11 @@ const Workflows = (props) => {
|
||||
<div>
|
||||
<b>Finished:</b> {endtime.toISOString()}
|
||||
</div>
|
||||
<div>
|
||||
<b>Last node:</b> {selectedExecution.last_node}
|
||||
</div>
|
||||
{/*
|
||||
<div>
|
||||
<b>Last Result:</b> {lastresult}
|
||||
</div>
|
||||
*/}
|
||||
<div style={{marginTop: 10}}>
|
||||
{arg}
|
||||
</div>
|
||||
@@ -827,7 +838,7 @@ const Workflows = (props) => {
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<DialogTitle><div style={{color: "white"}}>New workflow</div></DialogTitle>
|
||||
<DialogTitle><div style={{color: "white"}}>{editingWorkflow.id !== undefined ? "Editing" : "New"} workflow</div></DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
onBlur={(event) => setNewWorkflowName(event.target.value)}
|
||||
@@ -862,7 +873,15 @@ const Workflows = (props) => {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={{}} disabled={newWorkflowName.length === 0} onClick={() => {
|
||||
setNewWorkflow(newWorkflowName, newWorkflowDescription, {}, true)
|
||||
if (editingWorkflow.id !== undefined) {
|
||||
setNewWorkflow(newWorkflowName, newWorkflowDescription, editingWorkflow, false)
|
||||
setNewWorkflowName("")
|
||||
setNewWorkflowDescription("")
|
||||
setEditingWorkflow({})
|
||||
} else {
|
||||
setNewWorkflow(newWorkflowName, newWorkflowDescription, {}, true)
|
||||
}
|
||||
|
||||
setModalOpen(false)
|
||||
}} color="primary">
|
||||
Submit
|
||||
@@ -907,7 +926,7 @@ const Workflows = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
*/}
|
||||
<Tooltip color="primary" title={"Download workflows"} placement="top">
|
||||
<Tooltip color="primary" title={"Download / Import workflows"} placement="top">
|
||||
<Button color="primary" style={{}} variant="text" onClick={() => setLoadWorkflowsModalOpen(true)}>
|
||||
<CloudDownloadIcon />
|
||||
</Button>
|
||||
@@ -971,7 +990,7 @@ const Workflows = (props) => {
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<b>Shuffle</b> 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. <Link to="/docs/about" style={{textDecoration: "none", color: "#f85a3e"}}>Click here to learn more.</Link>
|
||||
<b>Shuffle</b> 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. <a href="/docs/about" style={{textDecoration: "none", color: "#f85a3e"}}>Click here to learn more.</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -113,6 +113,16 @@ const data = [{
|
||||
'background-color': '#77b0d0',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.skipped-highlight',
|
||||
css: {
|
||||
'background-color': 'grey',
|
||||
'border-color': 'grey',
|
||||
'border-width': '8px',
|
||||
'transition-property': 'background-color',
|
||||
'transition-duration': '0.5s',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.success-highlight',
|
||||
css: {
|
||||
|
||||
@@ -36,7 +36,8 @@ var orgId = os.Getenv("ORG_ID")
|
||||
var sleepTime = 3
|
||||
|
||||
// Timeout if somethinc rashes
|
||||
var workerTimeout = 600
|
||||
//var workerTimeout = 600
|
||||
var workerTimeout = 300
|
||||
|
||||
type ExecutionRequestWrapper struct {
|
||||
Data []ExecutionRequest `json:"data"`
|
||||
@@ -52,7 +53,7 @@ type ExecutionRequest struct {
|
||||
}
|
||||
|
||||
// Deploys the internal worker whenever something happens
|
||||
func deployWorker(cli *dockerclient.Client, image string, identifier string, env []string) error {
|
||||
func deployWorker(cli *dockerclient.Client, image string, identifier string, env []string) {
|
||||
// Binds is the actual "-v" volume.
|
||||
hostConfig := &container.HostConfig{
|
||||
LogConfig: container.LogConfig{
|
||||
@@ -101,16 +102,40 @@ func deployWorker(cli *dockerclient.Client, image string, identifier string, env
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
err = cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Failed to start container in environment %s: %s", environment, err)
|
||||
return
|
||||
|
||||
//stats, err := cli.ContainerInspect(context.Background(), containerName)
|
||||
//if err != nil {
|
||||
// log.Printf("Failed checking worker %s", containerName)
|
||||
// return
|
||||
//}
|
||||
|
||||
//containerStatus := stats.ContainerJSONBase.State.Status
|
||||
//if containerStatus != "running" {
|
||||
// log.Printf("Status of %s is %s. Should be running. Will reset", containerName, containerStatus)
|
||||
// err = stopWorker(containerName)
|
||||
// if err != nil {
|
||||
// log.Printf("Failed stopping worker %s", execution.ExecutionId)
|
||||
// return
|
||||
// }
|
||||
|
||||
// err = deployWorker(cli, workerImage, containerName, env)
|
||||
// if err != nil {
|
||||
// log.Printf("Failed executing worker %s in state %s", execution.ExecutionId, containerStatus)
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
} else {
|
||||
log.Printf("Container %s was created under environment %s", cont.ID, environment)
|
||||
}
|
||||
return nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func stopWorker(containername string) error {
|
||||
@@ -169,7 +194,7 @@ func initializeImages(dockercli *dockerclient.Client) {
|
||||
|
||||
// Initial loop etc
|
||||
func main() {
|
||||
zombiecheck()
|
||||
go zombiecheck()
|
||||
log.Println("Setting up execution environment")
|
||||
|
||||
//FIXME
|
||||
@@ -236,7 +261,7 @@ func main() {
|
||||
log.Printf("Failed making request: %s", err)
|
||||
zombiecounter += 1
|
||||
if zombiecounter*sleepTime > workerTimeout {
|
||||
zombiecheck()
|
||||
go zombiecheck()
|
||||
zombiecounter = 0
|
||||
}
|
||||
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||||
@@ -257,7 +282,7 @@ func main() {
|
||||
log.Printf("Failed reading body: %s", err)
|
||||
zombiecounter += 1
|
||||
if zombiecounter*sleepTime > workerTimeout {
|
||||
zombiecheck()
|
||||
go zombiecheck()
|
||||
zombiecounter = 0
|
||||
}
|
||||
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||||
@@ -271,7 +296,7 @@ func main() {
|
||||
sleepTime = 10
|
||||
zombiecounter += 1
|
||||
if zombiecounter*sleepTime > workerTimeout {
|
||||
zombiecheck()
|
||||
go zombiecheck()
|
||||
zombiecounter = 0
|
||||
}
|
||||
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||||
@@ -286,7 +311,7 @@ func main() {
|
||||
if len(executionRequests.Data) == 0 {
|
||||
zombiecounter += 1
|
||||
if zombiecounter*sleepTime > workerTimeout {
|
||||
zombiecheck()
|
||||
go zombiecheck()
|
||||
zombiecounter = 0
|
||||
}
|
||||
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||||
@@ -322,34 +347,9 @@ func main() {
|
||||
env = append(env, fmt.Sprintf("DOCKER_API_VERSION=%s", dockerApiVersion))
|
||||
}
|
||||
|
||||
err = deployWorker(dockercli, workerImage, containerName, env)
|
||||
if err != nil {
|
||||
stats, err := dockercli.ContainerInspect(context.Background(), containerName)
|
||||
if err != nil {
|
||||
log.Printf("Failed checking worker %s", execution.ExecutionId)
|
||||
continue
|
||||
}
|
||||
go deployWorker(dockercli, workerImage, containerName, env)
|
||||
|
||||
containerStatus := stats.ContainerJSONBase.State.Status
|
||||
if containerStatus != "running" {
|
||||
log.Printf("Status of %s is %s. Should be running. Will reset", containerName, containerStatus)
|
||||
err = stopWorker(containerName)
|
||||
if err != nil {
|
||||
log.Printf("Failed stopping worker %s", execution.ExecutionId)
|
||||
continue
|
||||
}
|
||||
|
||||
err = deployWorker(dockercli, workerImage, containerName, env)
|
||||
if err != nil {
|
||||
log.Printf("Failed executing worker %s in state %s", execution.ExecutionId, containerStatus)
|
||||
}
|
||||
} else {
|
||||
// Should basically never hit here rofl
|
||||
log.Printf("ERROR: I HAVE NO IDEA WHAT WENT WRONG. CHECK %s", containerName)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("%s is deployed and to being removed from queue.", execution.ExecutionId)
|
||||
log.Printf("%s is deployed and to be removed from queue.", execution.ExecutionId)
|
||||
zombiecounter += 1
|
||||
toBeRemoved.Data = append(toBeRemoved.Data, execution)
|
||||
}
|
||||
@@ -427,34 +427,54 @@ func zombiecheck() error {
|
||||
All: true,
|
||||
})
|
||||
|
||||
containerNames := map[string]string{}
|
||||
|
||||
stopContainers := []string{}
|
||||
removeContainers := []string{}
|
||||
for _, container := range containers {
|
||||
|
||||
// Skip random containers. Only handle things related to Shuffle.
|
||||
if !strings.Contains(container.Image, baseimagename) {
|
||||
shuffleFound := false
|
||||
for _, item := range container.Labels {
|
||||
if item == "shuffle" {
|
||||
shuffleFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check image name
|
||||
if !shuffleFound {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range container.Names {
|
||||
// FIXME - add name_version_uid_uid regex check as well
|
||||
if !strings.HasPrefix(name, "/worker") {
|
||||
if strings.HasPrefix(name, "/shuffle") {
|
||||
continue
|
||||
}
|
||||
|
||||
if container.State != "running" {
|
||||
removeContainers = append(removeContainers, container.ID)
|
||||
containerNames[container.ID] = name
|
||||
}
|
||||
|
||||
// stopcontainer & removecontainer
|
||||
currenttime := time.Now().Unix()
|
||||
//log.Printf("Time: %d - %d", currenttime-container.Created, int64(workerTimeout))
|
||||
if container.State == "running" && currenttime-container.Created > int64(workerTimeout) {
|
||||
stopContainers = append(stopContainers, container.ID)
|
||||
containerNames[container.ID] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME - add killing of apps with same execution ID too
|
||||
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)
|
||||
}
|
||||
log.Printf("Stopping and removing container %s", containerNames[containername])
|
||||
go dockercli.ContainerStop(ctx, containername, nil)
|
||||
removeContainers = append(removeContainers, containername)
|
||||
}
|
||||
|
||||
removeOptions := types.ContainerRemoveOptions{
|
||||
@@ -463,11 +483,7 @@ func zombiecheck() error {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
go dockercli.ContainerRemove(ctx, containername, removeOptions)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Binary file not shown.
@@ -57,38 +57,53 @@ type Org struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// FIXME: Generate a callback authentication ID?
|
||||
type WorkflowExecution struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
ExecutionId string `json:"execution_id"`
|
||||
ExecutionArgument string `json:"execution_argument"`
|
||||
WorkflowId string `json:"workflow_id"`
|
||||
LastNode string `json:"last_node"`
|
||||
Authorization string `json:"authorization"`
|
||||
Result string `json:"result"`
|
||||
StartedAt int64 `json:"started_at"`
|
||||
CompletedAt int64 `json:"completed_at"`
|
||||
ProjectId string `json:"project_id"`
|
||||
Locations []string `json:"locations"`
|
||||
Workflow Workflow `json:"workflow"`
|
||||
Results []ActionResult `json:"results"`
|
||||
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"`
|
||||
ExecutionId string `json:"execution_id" datastore:"execution_id"`
|
||||
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"`
|
||||
ID string `json:"id" datastore:"id"`
|
||||
Name string `json:"name" datastore:"name"`
|
||||
Value string `json:"value" datastore:"value"`
|
||||
} `json:"execution_variables,omitempty" datastore:"execution_variables,omitempty"`
|
||||
}
|
||||
|
||||
// Added environment for location to execute
|
||||
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" datastore:"isStartNode"`
|
||||
Label string `json:"label" datastore:"label"`
|
||||
Environment string `json:"environment" datastore:"environment"`
|
||||
Name string `json:"name" datastore:"name"`
|
||||
Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"`
|
||||
Position 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" datastore:"isStartNode"`
|
||||
Sharing bool `json:"sharing" datastore:"sharing"`
|
||||
PrivateID string `json:"private_id" datastore:"private_id"`
|
||||
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"`
|
||||
Name string `json:"name" datastore:"name"`
|
||||
Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"`
|
||||
ExecutionVariable struct {
|
||||
Description string `json:"description" datastore:"description"`
|
||||
ID string `json:"id" datastore:"id"`
|
||||
Name string `json:"name" datastore:"name"`
|
||||
Value string `json:"value" datastore:"value"`
|
||||
} `json:"execution_variable,omitempty" datastore:"execution_variable,omitempty"`
|
||||
Position struct {
|
||||
X float64 `json:"x" datastore:"x"`
|
||||
Y float64 `json:"y" datastore:"y"`
|
||||
} `json:"position"`
|
||||
@@ -132,10 +147,10 @@ type Trigger struct {
|
||||
}
|
||||
|
||||
type Workflow struct {
|
||||
Actions []Action `json:"actions" datastore:"actions"`
|
||||
Branches []Branch `json:"branches" datastore:"branches"`
|
||||
Triggers []Trigger `json:"triggers" datastore:"triggers"`
|
||||
Schedules []Schedule `json:"schedules" datastore:"schedules"`
|
||||
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"`
|
||||
Errors []string `json:"errors,omitempty" datastore:"errors"`
|
||||
Tags []string `json:"tags,omitempty" datastore:"tags"`
|
||||
ID string `json:"id" datastore:"id"`
|
||||
@@ -153,6 +168,12 @@ type Workflow struct {
|
||||
Name string `json:"name" datastore:"name"`
|
||||
Value string `json:"value" datastore:"value"`
|
||||
} `json:"workflow_variables" datastore:"workflow_variables"`
|
||||
ExecutionVariables []struct {
|
||||
Description string `json:"description" datastore:"description"`
|
||||
ID string `json:"id" datastore:"id"`
|
||||
Name string `json:"name" datastore:"name"`
|
||||
Value string `json:"value" datastore:"value"`
|
||||
} `json:"execution_variables,omitempty" datastore:"execution_variables,omitempty"`
|
||||
}
|
||||
|
||||
type ActionResult struct {
|
||||
@@ -196,22 +217,41 @@ type WorkflowAppActionParameter struct {
|
||||
} `json:"schema"`
|
||||
}
|
||||
|
||||
type AuthenticationStore struct {
|
||||
Key string `json:"key" datastore:"key"`
|
||||
Value string `json:"value" datastore:"value"`
|
||||
}
|
||||
|
||||
type WorkflowAppAction struct {
|
||||
Description string `json:"description" datastore:"description"`
|
||||
ID string `json:"id" datastore:"id"`
|
||||
Name string `json:"name" datastore:"name"`
|
||||
NodeType string `json:"node_type" datastore:"node_type"`
|
||||
Environment string `json:"environment" datastore:"environment"`
|
||||
Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"`
|
||||
Returns struct {
|
||||
Description string `json:"description" datastore:"returns"`
|
||||
Description string `json:"description" datastore:"description"`
|
||||
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"`
|
||||
Authentication []AuthenticationStore `json:"authentication" datastore:"authentication" 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"`
|
||||
ID string `json:"id" datastore:"id"`
|
||||
Schema struct {
|
||||
Type string `json:"type" datastore:"type"`
|
||||
} `json:"schema" datastore:"schema"`
|
||||
Name string `json:"name" datastore:"name"`
|
||||
Value string `json:"value" datastore:"value"`
|
||||
} `json:"execution_variable" datastore:"execution_variables"`
|
||||
Returns struct {
|
||||
Description string `json:"description" datastore:"returns" yaml:"description,omitempty"`
|
||||
ID string `json:"id" datastore:"id" yaml:"id,omitempty"`
|
||||
Schema SchemaDefinition `json:"schema" datastore:"schema" yaml:"schema"`
|
||||
} `json:"returns" datastore:"returns"`
|
||||
}
|
||||
|
||||
type SchemaDefinition struct {
|
||||
Type string `json:"type" datastore:"type"`
|
||||
}
|
||||
|
||||
// removes every container except itself (worker)
|
||||
func shutdown(executionId, workflowId string) {
|
||||
dockercli, err := dockerclient.NewEnvClient()
|
||||
@@ -245,9 +285,6 @@ func shutdown(executionId, workflowId string) {
|
||||
|
||||
}
|
||||
|
||||
// FIXME: Add an API call to the backend
|
||||
// fmt.Sprintf("AUTHORIZATION=%s", workflowExecution.Authorization),
|
||||
|
||||
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/executions/%s/abort", baseUrl, workflowId, executionId)
|
||||
req, err := http.NewRequest(
|
||||
"GET",
|
||||
@@ -259,6 +296,12 @@ func shutdown(executionId, workflowId string) {
|
||||
log.Println("Failed building request: %s", err)
|
||||
}
|
||||
|
||||
// FIXME: Add an API call to the backend
|
||||
authorization := os.Getenv("AUTHORIZATION")
|
||||
if len(authorization) > 0 {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization))
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
//req.Header.Add("Authorization", authorization)
|
||||
client := &http.Client{}
|
||||
@@ -295,7 +338,8 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
|
||||
},
|
||||
}
|
||||
} else {
|
||||
log.Printf("Bad config: %s. Using default network", baseUrl)
|
||||
// FIXME: Default config
|
||||
//log.Printf("Bad config: %s. Using default network", baseUrl)
|
||||
}
|
||||
|
||||
cont, err := cli.ContainerCreate(
|
||||
@@ -378,16 +422,39 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W
|
||||
}
|
||||
|
||||
onpremApps := []string{}
|
||||
startAction := workflowExecution.Workflow.Start
|
||||
startAction := workflowExecution.Start
|
||||
log.Printf("Startaction: %s", startAction)
|
||||
toExecuteOnprem := []string{}
|
||||
parents := map[string][]string{}
|
||||
children := map[string][]string{}
|
||||
|
||||
// source = parent, dest = child
|
||||
// source = parent node, dest = child node
|
||||
// parent can have more children, child can have more parents
|
||||
for _, branch := range workflowExecution.Workflow.Branches {
|
||||
parents[branch.DestinationID] = append(parents[branch.DestinationID], branch.SourceID)
|
||||
children[branch.SourceID] = append(children[branch.SourceID], branch.DestinationID)
|
||||
// Check what the parent is first. If it's trigger - skip
|
||||
sourceFound := false
|
||||
destinationFound := false
|
||||
for _, action := range workflowExecution.Workflow.Actions {
|
||||
if action.ID == branch.SourceID {
|
||||
sourceFound = true
|
||||
}
|
||||
|
||||
if action.ID == branch.DestinationID {
|
||||
destinationFound = true
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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("Actions: %d", len(workflowExecution.Workflow.Actions))
|
||||
@@ -437,37 +504,133 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W
|
||||
}
|
||||
|
||||
// Process the parents etc. How?
|
||||
// while queue:
|
||||
// while len(self.in_process) > 0 or len(self.parallel_in_process) > 0:
|
||||
// check if its their own turn to continue
|
||||
// visited = {self.start_action}
|
||||
visited := []string{}
|
||||
nextActions := []string{}
|
||||
queueNodes := []string{}
|
||||
|
||||
executed := []string{}
|
||||
nextActions := []string{startAction}
|
||||
firstIteration := true
|
||||
for {
|
||||
//if len(queueNodes) > 0 {
|
||||
// log.Println(queueNodes)
|
||||
// nextActions = queueNodes
|
||||
//} else {
|
||||
// nextActions := []string{}
|
||||
//}
|
||||
// FIXME - this might actually work, but probably not
|
||||
//queueNodes = []string{}
|
||||
queueNodes := []string{}
|
||||
|
||||
if len(workflowExecution.Results) == 0 {
|
||||
nextActions = []string{startAction}
|
||||
} else if firstIteration {
|
||||
firstIteration = false
|
||||
} else {
|
||||
// This is to re-check the nodes that exist and whether they should continue
|
||||
appendActions := []string{}
|
||||
for _, item := range workflowExecution.Results {
|
||||
visited = append(visited, item.Action.ID)
|
||||
|
||||
// FIXME: Check whether the item should be visited or not
|
||||
// Do the same check as in walkoff.go - are the parents done?
|
||||
// If skipped and both parents are skipped: keep as skipped, otherwise queue
|
||||
if item.Status == "SKIPPED" {
|
||||
isSkipped := true
|
||||
|
||||
for _, branch := range workflowExecution.Workflow.Branches {
|
||||
// 1. Finds branches where the destination is our node
|
||||
// 2. Finds results of those branches, and sees the status
|
||||
// 3. If the status isn't skipped or failure, then it will still run this node
|
||||
if branch.DestinationID == item.Action.ID {
|
||||
for _, subresult := range workflowExecution.Results {
|
||||
if subresult.Action.ID == branch.SourceID {
|
||||
if subresult.Status != "SKIPPED" && subresult.Status != "FAILURE" {
|
||||
log.Printf("\n\n\nSUBRESULT PARENT STATUS: %s\n\n\n", subresult.Status)
|
||||
isSkipped = false
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
visited = append(visited, item.Action.ID)
|
||||
}
|
||||
} else {
|
||||
log.Printf("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)
|
||||
visited = append(visited, item.Action.ID)
|
||||
}
|
||||
}
|
||||
|
||||
nextActions = children[item.Action.ID]
|
||||
// FIXME: check if nextActions items are finished?
|
||||
if len(appendActions) > 0 {
|
||||
log.Printf("APPENDED NODES: %#v", appendActions)
|
||||
nextActions = append(nextActions, appendActions...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is a backup in case something goes wrong in this complex hellhole.
|
||||
// Max default execution time is 5 minutes for now anyway, which should take
|
||||
// care if it gets stuck in a loop.
|
||||
// FIXME: Force killing a worker should result in a notification somewhere
|
||||
if len(nextActions) == 0 {
|
||||
log.Println("No next action. Finished?")
|
||||
//shutdown(workflowExecution.ExecutionId)
|
||||
log.Printf("No next action. Finished? Result vs Actions: %d - %d", len(workflowExecution.Results), len(workflowExecution.Workflow.Actions))
|
||||
if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions) {
|
||||
shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID)
|
||||
}
|
||||
|
||||
// Look for the NEXT missing action
|
||||
notFound := []string{}
|
||||
for _, action := range workflowExecution.Workflow.Actions {
|
||||
found := false
|
||||
for _, result := range workflowExecution.Results {
|
||||
if action.ID == result.Action.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
notFound = append(notFound, action.ID)
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("SOMETHING IS MISSING!: %#v", notFound)
|
||||
for _, item := range notFound {
|
||||
if arrayContains(executed, item) {
|
||||
log.Printf("%s has already executed but no result!", item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Visited means it's been touched in any way.
|
||||
outerIndex := -1
|
||||
for index, visit := range visited {
|
||||
if visit == item {
|
||||
outerIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if outerIndex >= 0 {
|
||||
log.Printf("Removing index %s from visited")
|
||||
visited = append(visited[:outerIndex], visited[outerIndex+1:]...)
|
||||
}
|
||||
|
||||
fixed := 0
|
||||
for _, parent := range parents[item] {
|
||||
parentResult := getResult(workflowExecution, parent)
|
||||
if parentResult.Status == "FINISHED" || parentResult.Status == "SUCCESS" || parentResult.Status == "SKIPPED" || parentResult.Status == "FAILURE" {
|
||||
fixed += 1
|
||||
}
|
||||
}
|
||||
|
||||
if fixed == len(parents[item]) {
|
||||
nextActions = append(nextActions, item)
|
||||
}
|
||||
|
||||
// If it's not executed and not in nextActions
|
||||
// FIXME: Check if the item's parents are finished. If they're not, skip.
|
||||
}
|
||||
}
|
||||
|
||||
for _, node := range nextActions {
|
||||
@@ -478,28 +641,17 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//log.Println(queueNodes)
|
||||
//log.Printf("NEXT: %s", nextActions)
|
||||
//log.Printf("queueNodes: %s", queueNodes)
|
||||
|
||||
// IF NOT VISITED && IN toExecuteOnPrem
|
||||
// SKIP if it's not onprem
|
||||
// FIXME: Find next node(s)
|
||||
//for _, result := range workflowExecution.Results {
|
||||
// log.Println(result.Status)
|
||||
//}
|
||||
|
||||
for _, nextAction := range nextActions {
|
||||
action := getAction(workflowExecution, nextAction)
|
||||
// FIXME - remove this. Should always need to be valid.
|
||||
//if action.IsValid == false {
|
||||
// log.Printf("%#v", action)
|
||||
// log.Printf("Action %s (%s) isn't valid. Exiting, BUT SHOULD CALLBACK TO SET FAILURE.", action.ID, action.Name)
|
||||
// os.Exit(3)
|
||||
//}
|
||||
|
||||
// check visited and onprem
|
||||
if arrayContains(visited, nextAction) {
|
||||
log.Printf("ALREADY VISITIED: %s", nextAction)
|
||||
log.Printf("ALREADY VISITIED (%s): %s", action.Label, nextAction)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -523,7 +675,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W
|
||||
fixed := 0
|
||||
for _, parent := range parents[nextAction] {
|
||||
parentResult := getResult(workflowExecution, parent)
|
||||
if parentResult.Status == "FINISHED" || parentResult.Status == "SUCCESS" {
|
||||
if parentResult.Status == "FINISHED" || parentResult.Status == "SUCCESS" || parentResult.Status == "SKIPPED" || parentResult.Status == "FAILURE" {
|
||||
fixed += 1
|
||||
}
|
||||
}
|
||||
@@ -536,7 +688,13 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W
|
||||
}
|
||||
|
||||
if continueOuter {
|
||||
log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", "))
|
||||
//log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", "))
|
||||
//for _, tmpaction := range parents[nextAction] {
|
||||
// action := getAction(workflowExecution, tmpaction)
|
||||
// _ = action
|
||||
// //log.Printf("Parent: %s", action.Label)
|
||||
//}
|
||||
// Find the result of the nodes?
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -590,7 +748,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W
|
||||
}
|
||||
|
||||
// marshal action and put it in there rofl
|
||||
log.Printf("Time to execute %s with app %s:%s, function %s, env %s with %d parameters.", action.ID, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters))
|
||||
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))
|
||||
|
||||
actionData, err := json.Marshal(action)
|
||||
if err != nil {
|
||||
@@ -618,11 +776,17 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W
|
||||
if err != nil {
|
||||
log.Printf("Failed deploying %s from image %s: %s", identifier, image, err)
|
||||
log.Printf("Should send status and exit the entire thing?")
|
||||
//shutdown(workflowExecution.ExecutionId)
|
||||
//shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID)
|
||||
}
|
||||
|
||||
log.Printf("Adding visited (3): %s", action.Label)
|
||||
|
||||
visited = append(visited, action.ID)
|
||||
//log.Printf("%#v", action)
|
||||
executed = append(executed, action.ID)
|
||||
|
||||
// If children of action.ID are NOT in executed:
|
||||
// Remove them from visited.
|
||||
//log.Printf("EXECUTED: %#v", executed)
|
||||
}
|
||||
|
||||
//log.Println(nextAction)
|
||||
@@ -774,14 +938,64 @@ func getAction(workflowExecution WorkflowExecution, id string) Action {
|
||||
return Action{}
|
||||
}
|
||||
|
||||
func runTestExecution(client *http.Client, workflowId, apikey string) (string, string) {
|
||||
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", baseUrl, workflowId)
|
||||
req, err := http.NewRequest(
|
||||
"GET",
|
||||
fullUrl,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error building test request: %s", err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apikey))
|
||||
newresp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Error running test request: %s", err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(newresp.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed reading body: %s", err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
log.Printf("Body: %s", string(body))
|
||||
var workflowExecution WorkflowExecution
|
||||
err = json.Unmarshal(body, &workflowExecution)
|
||||
if err != nil {
|
||||
log.Printf("Failed workflowExecution unmarshal: %s", err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return workflowExecution.Authorization, workflowExecution.ExecutionId
|
||||
}
|
||||
|
||||
// Initial loop etc
|
||||
func main() {
|
||||
log.Printf("Setting up worker environment")
|
||||
|
||||
sleepTime := 5
|
||||
client := &http.Client{}
|
||||
authorization := os.Getenv("AUTHORIZATION")
|
||||
executionId := os.Getenv("EXECUTIONID")
|
||||
|
||||
// WORKER_TESTING_WORKFLOW should be a workflow ID
|
||||
authorization := ""
|
||||
executionId := ""
|
||||
testing := os.Getenv("WORKER_TESTING_WORKFLOW")
|
||||
shuffle_apikey := os.Getenv("WORKER_TESTING_APIKEY")
|
||||
if len(testing) > 0 && len(shuffle_apikey) > 0 {
|
||||
// Execute a workflow and use that info
|
||||
log.Printf("!! Running test environment for worker by executing workflow %s", testing)
|
||||
authorization, executionId = runTestExecution(client, testing, shuffle_apikey)
|
||||
|
||||
//os.Exit(3)
|
||||
} else {
|
||||
authorization = os.Getenv("AUTHORIZATION")
|
||||
executionId = os.Getenv("EXECUTIONID")
|
||||
}
|
||||
|
||||
if len(authorization) == 0 {
|
||||
log.Println("No AUTHORIZATION key set in env")
|
||||
|
||||
Reference in New Issue
Block a user