Merge pull request #72 from frikky/1.0.0

Execution model overhaul. Executions can now start ~anywhere.
This commit is contained in:
Frikky
2020-06-21 04:17:48 +09:00
committed by GitHub
22 changed files with 10377 additions and 12470 deletions
+1
View File
@@ -18,3 +18,4 @@ functions/generated_apps
backend/onprem/app_sdk/apps
*test.py
shuffle-apps/*
+288 -22
View File
@@ -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())
+8 -3
View File
@@ -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
+5 -11
View File
@@ -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",
},
+7 -7
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -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
+8137 -11825
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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: "/"})
}
+64 -5
View File
@@ -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 =
+6 -2
View File
@@ -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>
&nbsp;- <a href="https://github.com/frikky/security-openapis" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">Security API's</a>
&nbsp;- <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}
+2 -2
View File
@@ -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>
)
+10 -3
View File
@@ -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
View File
@@ -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>
+10
View File
@@ -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: {
+63 -47
View File
@@ -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.
+306 -92
View File
@@ -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")